mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-03-20 08:13:53 +01:00
Create WireMock.Net.MimePart project (#1300)
* Create WireMock.Net.MimePart project * . * REFACTOR * ILRepack * -- * ... * x * x * . * fix * public class MimePartMatcher * shared * min * . * <!--<DelaySign>true</DelaySign>--> * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
16
src/WireMock.Net.Shared/Constants/RegexConstants.cs
Normal file
16
src/WireMock.Net.Shared/Constants/RegexConstants.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
|
||||
namespace WireMock.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Some constants for Regex.
|
||||
/// </summary>
|
||||
internal static class RegexConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// The default timeout for regex operations.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
36
src/WireMock.Net.Shared/Exceptions/WireMockException.cs
Normal file
36
src/WireMock.Net.Shared/Exceptions/WireMockException.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
|
||||
namespace WireMock.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// WireMockException
|
||||
/// </summary>
|
||||
/// <seealso cref="Exception" />
|
||||
public class WireMockException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WireMockException"/> class.
|
||||
/// </summary>
|
||||
public WireMockException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WireMockException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public WireMockException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WireMockException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="inner">The inner.</param>
|
||||
public WireMockException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
54
src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs
Normal file
54
src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AnyOfTypes;
|
||||
using WireMock.Models;
|
||||
|
||||
namespace WireMock.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Some extensions for AnyOf.
|
||||
/// </summary>
|
||||
public static class AnyOfExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the pattern.
|
||||
/// </summary>
|
||||
/// <param name="value">AnyOf type</param>
|
||||
/// <returns>string value</returns>
|
||||
public static string GetPattern(this AnyOf<string, StringPattern> value)
|
||||
{
|
||||
return value.IsFirst ? value.First : value.Second.Pattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patterns.
|
||||
/// </summary>
|
||||
/// <param name="values">AnyOf types</param>
|
||||
/// <returns>string values</returns>
|
||||
public static string[] GetPatterns(this AnyOf<string, StringPattern>[] values)
|
||||
{
|
||||
return values.Select(GetPattern).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string-patterns to AnyOf patterns.
|
||||
/// </summary>
|
||||
/// <param name="patterns">The string patterns</param>
|
||||
/// <returns>The AnyOf patterns</returns>
|
||||
public static AnyOf<string, StringPattern>[] ToAnyOfPatterns(this IEnumerable<string> patterns)
|
||||
{
|
||||
return patterns.Select(p => p.ToAnyOfPattern()).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string-pattern to AnyOf pattern.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The string pattern</param>
|
||||
/// <returns>The AnyOf pattern</returns>
|
||||
public static AnyOf<string, StringPattern> ToAnyOfPattern(this string pattern)
|
||||
{
|
||||
return new AnyOf<string, StringPattern>(pattern);
|
||||
}
|
||||
}
|
||||
31
src/WireMock.Net.Shared/Extensions/EnumExtensions.cs
Normal file
31
src/WireMock.Net.Shared/Extensions/EnumExtensions.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace WireMock.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Some extension methods for Enums.
|
||||
/// </summary>
|
||||
internal static class EnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the fully qualified enum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
/// <param name="enumValue">The value.</param>
|
||||
/// <returns>The fully qualified enum value.</returns>
|
||||
public static string GetFullyQualifiedEnumValue<T>(this T enumValue)
|
||||
where T : struct, IConvertible
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
||||
if (!type.GetTypeInfo().IsEnum)
|
||||
{
|
||||
throw new ArgumentException("T must be an enum");
|
||||
}
|
||||
|
||||
return $"{type.Namespace}.{type.Name}.{enumValue}";
|
||||
}
|
||||
}
|
||||
18
src/WireMock.Net.Shared/Extensions/ExceptionExtensions.cs
Normal file
18
src/WireMock.Net.Shared/Extensions/ExceptionExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
|
||||
namespace WireMock.Extensions;
|
||||
|
||||
internal static class ExceptionExtensions
|
||||
{
|
||||
public static Exception? ToException(this Exception[] exceptions)
|
||||
{
|
||||
return exceptions.Length switch
|
||||
{
|
||||
1 => exceptions[0],
|
||||
> 1 => new AggregateException(exceptions),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
123
src/WireMock.Net.Shared/Http/HttpKnownHeaderNames.cs
Normal file
123
src/WireMock.Net.Shared/Http/HttpKnownHeaderNames.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace WireMock.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Copied from https://raw.githubusercontent.com/dotnet/corefx/master/src/Common/src/System/Net/HttpKnownHeaderNames.cs
|
||||
/// </summary>
|
||||
internal static class HttpKnownHeaderNames
|
||||
{
|
||||
// - https://docs.microsoft.com/en-us/dotnet/api/system.net.webheadercollection.isrestricted
|
||||
// - ContentLength is allowed per #720
|
||||
private static readonly string[] RestrictedResponseHeaders =
|
||||
[
|
||||
Accept,
|
||||
Connection,
|
||||
ContentType,
|
||||
Date, // RFC1123Pattern
|
||||
Expect,
|
||||
Host,
|
||||
IfModifiedSince,
|
||||
Range,
|
||||
Referer,
|
||||
TransferEncoding,
|
||||
UserAgent,
|
||||
ProxyConnection
|
||||
];
|
||||
|
||||
/// <summary>Tests whether the specified HTTP header can be set for the response.</summary>
|
||||
/// <param name="headerName">The header to test.</param>
|
||||
/// <returns>true if the header is restricted; otherwise, false.</returns>
|
||||
public static bool IsRestrictedResponseHeader(string headerName) => RestrictedResponseHeaders.Contains(headerName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public const string Accept = "Accept";
|
||||
public const string AcceptCharset = "Accept-Charset";
|
||||
public const string AcceptEncoding = "Accept-Encoding";
|
||||
public const string AcceptLanguage = "Accept-Language";
|
||||
public const string AcceptPatch = "Accept-Patch";
|
||||
public const string AcceptRanges = "Accept-Ranges";
|
||||
public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
|
||||
public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
|
||||
public const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
|
||||
public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
|
||||
public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
|
||||
public const string AccessControlMaxAge = "Access-Control-Max-Age";
|
||||
public const string Age = "Age";
|
||||
public const string Allow = "Allow";
|
||||
public const string AltSvc = "Alt-Svc";
|
||||
public const string Authorization = "Authorization";
|
||||
public const string CacheControl = "Cache-Control";
|
||||
public const string Connection = "Connection";
|
||||
public const string ContentDisposition = "Content-Disposition";
|
||||
public const string ContentEncoding = "Content-Encoding";
|
||||
public const string ContentLanguage = "Content-Language";
|
||||
public const string ContentLength = "Content-Length";
|
||||
public const string ContentLocation = "Content-Location";
|
||||
public const string ContentMD5 = "Content-MD5";
|
||||
public const string ContentRange = "Content-Range";
|
||||
public const string ContentSecurityPolicy = "Content-Security-Policy";
|
||||
public const string ContentType = "Content-Type";
|
||||
public const string Cookie = "Cookie";
|
||||
public const string Cookie2 = "Cookie2";
|
||||
public const string Date = "Date";
|
||||
public const string ETag = "ETag";
|
||||
public const string Expect = "Expect";
|
||||
public const string Expires = "Expires";
|
||||
public const string From = "From";
|
||||
public const string Host = "Host";
|
||||
public const string IfMatch = "If-Match";
|
||||
public const string IfModifiedSince = "If-Modified-Since";
|
||||
public const string IfNoneMatch = "If-None-Match";
|
||||
public const string IfRange = "If-Range";
|
||||
public const string IfUnmodifiedSince = "If-Unmodified-Since";
|
||||
public const string KeepAlive = "Keep-Alive";
|
||||
public const string LastModified = "Last-Modified";
|
||||
public const string Link = "Link";
|
||||
public const string Location = "Location";
|
||||
public const string MaxForwards = "Max-Forwards";
|
||||
public const string Origin = "Origin";
|
||||
public const string P3P = "P3P";
|
||||
public const string Pragma = "Pragma";
|
||||
public const string ProxyAuthenticate = "Proxy-Authenticate";
|
||||
public const string ProxyAuthorization = "Proxy-Authorization";
|
||||
public const string ProxyConnection = "Proxy-Connection";
|
||||
public const string PublicKeyPins = "Public-Key-Pins";
|
||||
public const string Range = "Range";
|
||||
public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched.
|
||||
public const string RetryAfter = "Retry-After";
|
||||
public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
|
||||
public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions";
|
||||
public const string SecWebSocketKey = "Sec-WebSocket-Key";
|
||||
public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
|
||||
public const string SecWebSocketVersion = "Sec-WebSocket-Version";
|
||||
public const string Server = "Server";
|
||||
public const string SetCookie = "Set-Cookie";
|
||||
public const string SetCookie2 = "Set-Cookie2";
|
||||
public const string StrictTransportSecurity = "Strict-Transport-Security";
|
||||
public const string TE = "TE";
|
||||
public const string TSV = "TSV";
|
||||
public const string Trailer = "Trailer";
|
||||
public const string TransferEncoding = "Transfer-Encoding";
|
||||
public const string Upgrade = "Upgrade";
|
||||
public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests";
|
||||
public const string UserAgent = "User-Agent";
|
||||
public const string Vary = "Vary";
|
||||
public const string Via = "Via";
|
||||
public const string WWWAuthenticate = "WWW-Authenticate";
|
||||
public const string Warning = "Warning";
|
||||
public const string XAspNetVersion = "X-AspNet-Version";
|
||||
public const string XContentDuration = "X-Content-Duration";
|
||||
public const string XContentTypeOptions = "X-Content-Type-Options";
|
||||
public const string XFrameOptions = "X-Frame-Options";
|
||||
public const string XMSEdgeRef = "X-MSEdge-Ref";
|
||||
public const string XPoweredBy = "X-Powered-By";
|
||||
public const string XRequestID = "X-Request-ID";
|
||||
public const string XUACompatible = "X-UA-Compatible";
|
||||
}
|
||||
82
src/WireMock.Net.Shared/Matchers/ExactObjectMatcher.cs
Normal file
82
src/WireMock.Net.Shared/Matchers/ExactObjectMatcher.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Linq;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// ExactObjectMatcher
|
||||
/// </summary>
|
||||
/// <seealso cref="IObjectMatcher" />
|
||||
public class ExactObjectMatcher : IObjectMatcher
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public object Value { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchBehaviour MatchBehaviour { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExactObjectMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public ExactObjectMatcher(object value) : this(MatchBehaviour.AcceptOnMatch, value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExactObjectMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="matchBehaviour">The match behaviour.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public ExactObjectMatcher(MatchBehaviour matchBehaviour, object value)
|
||||
{
|
||||
Value = Guard.NotNull(value);
|
||||
MatchBehaviour = matchBehaviour;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExactObjectMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public ExactObjectMatcher(byte[] value) : this(MatchBehaviour.AcceptOnMatch, value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExactObjectMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="matchBehaviour">The match behaviour.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public ExactObjectMatcher(MatchBehaviour matchBehaviour, byte[] value)
|
||||
{
|
||||
Value = Guard.NotNull(value);
|
||||
MatchBehaviour = matchBehaviour;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchResult IsMatch(object? input)
|
||||
{
|
||||
bool equals;
|
||||
if (Value is byte[] valueAsBytes && input is byte[] inputAsBytes)
|
||||
{
|
||||
equals = valueAsBytes.SequenceEqual(inputAsBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
equals = Equals(Value, input);
|
||||
}
|
||||
|
||||
return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(equals));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(ExactObjectMatcher);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCSharpCodeArguments()
|
||||
{
|
||||
return "NotImplemented";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Stef.Validation;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Matchers.Helpers;
|
||||
|
||||
internal static class BodyDataMatchScoreCalculator
|
||||
{
|
||||
internal static MatchResult CalculateMatchScore(IBodyData? requestMessage, IMatcher matcher)
|
||||
{
|
||||
Guard.NotNull(matcher);
|
||||
|
||||
if (requestMessage == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (matcher is NotNullOrEmptyMatcher notNullOrEmptyMatcher)
|
||||
{
|
||||
switch (requestMessage.DetectedBodyType)
|
||||
{
|
||||
case BodyType.Json:
|
||||
case BodyType.String:
|
||||
case BodyType.FormUrlEncoded:
|
||||
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyAsString);
|
||||
|
||||
case BodyType.Bytes:
|
||||
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyAsBytes);
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
if (matcher is ExactObjectMatcher exactObjectMatcher)
|
||||
{
|
||||
// If the body is a byte array, try to match.
|
||||
var detectedBodyType = requestMessage.DetectedBodyType;
|
||||
if (detectedBodyType is BodyType.Bytes or BodyType.String or BodyType.FormUrlEncoded)
|
||||
{
|
||||
return exactObjectMatcher.IsMatch(requestMessage.BodyAsBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the matcher is a IObjectMatcher
|
||||
if (matcher is IObjectMatcher objectMatcher)
|
||||
{
|
||||
// If the body is a JSON object, try to match.
|
||||
if (requestMessage.DetectedBodyType == BodyType.Json)
|
||||
{
|
||||
return objectMatcher.IsMatch(requestMessage.BodyAsJson);
|
||||
}
|
||||
|
||||
// If the body is a byte array, try to match.
|
||||
if (requestMessage.DetectedBodyType == BodyType.Bytes)
|
||||
{
|
||||
return objectMatcher.IsMatch(requestMessage.BodyAsBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// In case the matcher is a IStringMatcher and If body is a Json or a String, use the BodyAsString to match on.
|
||||
if (matcher is IStringMatcher stringMatcher && requestMessage.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded)
|
||||
{
|
||||
return stringMatcher.IsMatch(requestMessage.BodyAsString);
|
||||
}
|
||||
|
||||
// In case the matcher is a IProtoBufMatcher, use the BodyAsBytes to match on.
|
||||
if (matcher is IProtoBufMatcher protoBufMatcher)
|
||||
{
|
||||
return protoBufMatcher.IsMatchAsync(requestMessage.BodyAsBytes).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
20
src/WireMock.Net.Shared/Matchers/IBytesMatcher.cs
Normal file
20
src/WireMock.Net.Shared/Matchers/IBytesMatcher.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IBytesMatcher
|
||||
/// </summary>
|
||||
public interface IBytesMatcher : IMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified input is match.
|
||||
/// </summary>
|
||||
/// <param name="input">The input byte array.</param>
|
||||
/// <param name="cancellationToken">The CancellationToken [optional].</param>
|
||||
/// <returns>MatchResult</returns>
|
||||
Task<MatchResult> IsMatchAsync(byte[]? input, CancellationToken cancellationToken = default);
|
||||
}
|
||||
12
src/WireMock.Net.Shared/Matchers/ICSharpCodeMatcher.cs
Normal file
12
src/WireMock.Net.Shared/Matchers/ICSharpCodeMatcher.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// CSharpCode / CS-Script Matcher
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IObjectMatcher"/>
|
||||
/// <inheritdoc cref="IStringMatcher"/>
|
||||
public interface ICSharpCodeMatcher : IObjectMatcher, IStringMatcher
|
||||
{
|
||||
}
|
||||
20
src/WireMock.Net.Shared/Matchers/IDecodeBytesMatcher.cs
Normal file
20
src/WireMock.Net.Shared/Matchers/IDecodeBytesMatcher.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IDecodeBytesMatcher
|
||||
/// </summary>
|
||||
public interface IDecodeBytesMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Decode byte array to an object.
|
||||
/// </summary>
|
||||
/// <param name="input">The byte array</param>
|
||||
/// <param name="cancellationToken">The CancellationToken [optional].</param>
|
||||
/// <returns>object</returns>
|
||||
Task<object?> DecodeAsync(byte[]? input, CancellationToken cancellationToken = default);
|
||||
}
|
||||
15
src/WireMock.Net.Shared/Matchers/IIgnoreCaseMatcher.cs
Normal file
15
src/WireMock.Net.Shared/Matchers/IIgnoreCaseMatcher.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IIgnoreCaseMatcher
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IMatcher"/>
|
||||
public interface IIgnoreCaseMatcher : IMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignore the case from the pattern.
|
||||
/// </summary>
|
||||
bool IgnoreCase { get; }
|
||||
}
|
||||
25
src/WireMock.Net.Shared/Matchers/IMatcher.cs
Normal file
25
src/WireMock.Net.Shared/Matchers/IMatcher.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IMatcher
|
||||
/// </summary>
|
||||
public interface IMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the match behaviour.
|
||||
/// </summary>
|
||||
MatchBehaviour MatchBehaviour { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the C# code arguments.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetCSharpCodeArguments();
|
||||
}
|
||||
37
src/WireMock.Net.Shared/Matchers/IMimePartMatcher.cs
Normal file
37
src/WireMock.Net.Shared/Matchers/IMimePartMatcher.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// MimePartMatcher
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IMatcher"/>
|
||||
public interface IMimePartMatcher : IMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// ContentType Matcher (image/png; name=image.png.)
|
||||
/// </summary>
|
||||
IStringMatcher? ContentTypeMatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ContentDisposition Matcher (attachment; filename=image.png)
|
||||
/// </summary>
|
||||
IStringMatcher? ContentDispositionMatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ContentTransferEncoding Matcher (base64)
|
||||
/// </summary>
|
||||
IStringMatcher? ContentTransferEncodingMatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Content Matcher
|
||||
/// </summary>
|
||||
IMatcher? ContentMatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified MimePart is match.
|
||||
/// </summary>
|
||||
/// <param name="value">The MimePart.</param>
|
||||
/// <returns>A value between 0.0 - 1.0 of the similarity.</returns>
|
||||
public MatchResult IsMatch(object value);
|
||||
}
|
||||
22
src/WireMock.Net.Shared/Matchers/IObjectMatcher.cs
Normal file
22
src/WireMock.Net.Shared/Matchers/IObjectMatcher.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IObjectMatcher
|
||||
/// </summary>
|
||||
public interface IObjectMatcher : IMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value (can be a string or an object).
|
||||
/// </summary>
|
||||
/// <returns>Value</returns>
|
||||
object Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified input is match.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns>MatchResult</returns>
|
||||
MatchResult IsMatch(object? input);
|
||||
}
|
||||
10
src/WireMock.Net.Shared/Matchers/IProtoBufMatcher.cs
Normal file
10
src/WireMock.Net.Shared/Matchers/IProtoBufMatcher.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IProtoBufMatcher
|
||||
/// </summary>
|
||||
public interface IProtoBufMatcher : IDecodeBytesMatcher, IBytesMatcher
|
||||
{
|
||||
}
|
||||
31
src/WireMock.Net.Shared/Matchers/IStringMatcher.cs
Normal file
31
src/WireMock.Net.Shared/Matchers/IStringMatcher.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using AnyOfTypes;
|
||||
using WireMock.Models;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// IStringMatcher
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IMatcher"/>
|
||||
public interface IStringMatcher : IMatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified input is match.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns>MatchResult</returns>
|
||||
MatchResult IsMatch(string? input);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patterns.
|
||||
/// </summary>
|
||||
/// <returns>Patterns</returns>
|
||||
AnyOf<string, StringPattern>[] GetPatterns();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Matchers.MatchOperator"/>.
|
||||
/// </summary>
|
||||
MatchOperator MatchOperator { get; }
|
||||
}
|
||||
19
src/WireMock.Net.Shared/Matchers/MatchBehaviour.cs
Normal file
19
src/WireMock.Net.Shared/Matchers/MatchBehaviour.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// MatchBehaviour (Accept or Reject)
|
||||
/// </summary>
|
||||
public enum MatchBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Accept on match (default)
|
||||
/// </summary>
|
||||
AcceptOnMatch,
|
||||
|
||||
/// <summary>
|
||||
/// Reject on match
|
||||
/// </summary>
|
||||
RejectOnMatch
|
||||
}
|
||||
41
src/WireMock.Net.Shared/Matchers/MatchBehaviourHelper.cs
Normal file
41
src/WireMock.Net.Shared/Matchers/MatchBehaviourHelper.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// MatchBehaviourHelper
|
||||
/// </summary>
|
||||
internal static class MatchBehaviourHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the specified match behaviour and match value to a new match value.
|
||||
///
|
||||
/// if AcceptOnMatch --> return match (default)
|
||||
/// if RejectOnMatch and match = 0.0 --> return 1.0
|
||||
/// if RejectOnMatch and match = 0.? --> return 0.0
|
||||
/// if RejectOnMatch and match = 1.0 --> return 0.0
|
||||
/// </summary>
|
||||
/// <param name="matchBehaviour">The match behaviour.</param>
|
||||
/// <param name="match">The match.</param>
|
||||
/// <returns>match value</returns>
|
||||
internal static double Convert(MatchBehaviour matchBehaviour, double match)
|
||||
{
|
||||
if (matchBehaviour == MatchBehaviour.AcceptOnMatch)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
return match <= MatchScores.Tolerance ? MatchScores.Perfect : MatchScores.Mismatch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the specified match behaviour and match result to a new match result value.
|
||||
/// </summary>
|
||||
/// <param name="matchBehaviour">The match behaviour.</param>
|
||||
/// <param name="result">The match result.</param>
|
||||
/// <returns>match result</returns>
|
||||
internal static MatchResult Convert(MatchBehaviour matchBehaviour, MatchResult result)
|
||||
{
|
||||
return matchBehaviour == MatchBehaviour.AcceptOnMatch ? result : new MatchResult(Convert(matchBehaviour, result.Score), result.Exception);
|
||||
}
|
||||
}
|
||||
24
src/WireMock.Net.Shared/Matchers/MatchOperator.cs
Normal file
24
src/WireMock.Net.Shared/Matchers/MatchOperator.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// The Operator to use when multiple patterns are defined.
|
||||
/// </summary>
|
||||
public enum MatchOperator
|
||||
{
|
||||
/// <summary>
|
||||
/// Only one pattern needs to match. [Default]
|
||||
/// </summary>
|
||||
Or,
|
||||
|
||||
/// <summary>
|
||||
/// All patterns should match.
|
||||
/// </summary>
|
||||
And,
|
||||
|
||||
/// <summary>
|
||||
/// The average value from all patterns.
|
||||
/// </summary>
|
||||
Average
|
||||
}
|
||||
91
src/WireMock.Net.Shared/Matchers/MatchResult.cs
Normal file
91
src/WireMock.Net.Shared/Matchers/MatchResult.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Stef.Validation;
|
||||
using WireMock.Extensions;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// The MatchResult which contains the score (value between 0.0 - 1.0 of the similarity) and an optional error message.
|
||||
/// </summary>
|
||||
public struct MatchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// A value between 0.0 - 1.0 of the similarity.
|
||||
/// </summary>
|
||||
public double Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The exception message) in case the matching fails.
|
||||
/// [Optional]
|
||||
/// </summary>
|
||||
public Exception? Exception { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a MatchResult
|
||||
/// </summary>
|
||||
/// <param name="score">A value between 0.0 - 1.0 of the similarity.</param>
|
||||
/// <param name="exception">The exception in case the matching fails. [Optional]</param>
|
||||
public MatchResult(double score, Exception? exception = null)
|
||||
{
|
||||
Score = score;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a MatchResult
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception in case the matching fails.</param>
|
||||
public MatchResult(Exception exception)
|
||||
{
|
||||
Exception = Guard.NotNull(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a double to a MatchResult.
|
||||
/// </summary>
|
||||
/// <param name="score">The score</param>
|
||||
public static implicit operator MatchResult(double score)
|
||||
{
|
||||
return new MatchResult(score);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the value a perfect match?
|
||||
/// </summary>
|
||||
public bool IsPerfect() => MatchScores.IsPerfect(Score);
|
||||
|
||||
/// <summary>
|
||||
/// Create a MatchResult from multiple MatchResults.
|
||||
/// </summary>
|
||||
/// <param name="matchResults">A list of MatchResults.</param>
|
||||
/// <param name="matchOperator">The MatchOperator</param>
|
||||
/// <returns>MatchResult</returns>
|
||||
public static MatchResult From(IReadOnlyList<MatchResult> matchResults, MatchOperator matchOperator)
|
||||
{
|
||||
Guard.NotNullOrEmpty(matchResults);
|
||||
|
||||
if (matchResults.Count == 1)
|
||||
{
|
||||
return matchResults[0];
|
||||
}
|
||||
|
||||
return new MatchResult
|
||||
{
|
||||
Score = MatchScores.ToScore(matchResults.Select(r => r.Score).ToArray(), matchOperator),
|
||||
Exception = matchResults.Select(m => m.Exception).OfType<Exception>().ToArray().ToException()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expand to Tuple
|
||||
/// </summary>
|
||||
/// <returns>Tuple : Score and Exception</returns>
|
||||
public (double Score, Exception? Exception) Expand()
|
||||
{
|
||||
return (Score, Exception);
|
||||
}
|
||||
}
|
||||
85
src/WireMock.Net.Shared/Matchers/MatchScores.cs
Normal file
85
src/WireMock.Net.Shared/Matchers/MatchScores.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// MatchScores
|
||||
/// </summary>
|
||||
public static class MatchScores
|
||||
{
|
||||
/// <summary>
|
||||
/// The tolerance
|
||||
/// </summary>
|
||||
public const double Tolerance = 0.000001;
|
||||
|
||||
/// <summary>
|
||||
/// The default mismatch score
|
||||
/// </summary>
|
||||
public const double Mismatch = 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// The default perfect match score
|
||||
/// </summary>
|
||||
public const double Perfect = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// The almost perfect match score
|
||||
/// </summary>
|
||||
public const double AlmostPerfect = 0.99;
|
||||
|
||||
/// <summary>
|
||||
/// Is the value a perfect match?
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>true/false</returns>
|
||||
public static bool IsPerfect(double value)
|
||||
{
|
||||
return Math.Abs(value - Perfect) < Tolerance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a bool to the score.
|
||||
/// </summary>
|
||||
/// <param name="value">if set to <c>true</c> [value].</param>
|
||||
/// <returns>score</returns>
|
||||
public static double ToScore(bool value)
|
||||
{
|
||||
return value ? Perfect : Mismatch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the score from multiple values.
|
||||
/// </summary>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <param name="matchOperator">The <see cref="MatchOperator"/>.</param>
|
||||
/// <returns>average score</returns>
|
||||
public static double ToScore(IReadOnlyCollection<bool> values, MatchOperator matchOperator)
|
||||
{
|
||||
return ToScore(values.Select(ToScore).ToArray(), matchOperator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the score from multiple values.
|
||||
/// </summary>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <param name="matchOperator"></param>
|
||||
/// <returns>average score</returns>
|
||||
public static double ToScore(IReadOnlyCollection<double> values, MatchOperator matchOperator)
|
||||
{
|
||||
if (!values.Any())
|
||||
{
|
||||
return Mismatch;
|
||||
}
|
||||
|
||||
return matchOperator switch
|
||||
{
|
||||
MatchOperator.Or => ToScore(values.Any(IsPerfect)),
|
||||
MatchOperator.And => ToScore(values.All(IsPerfect)),
|
||||
_ => values.Average()
|
||||
};
|
||||
}
|
||||
}
|
||||
84
src/WireMock.Net.Shared/Matchers/NotNullOrEmptyMatcher.cs
Normal file
84
src/WireMock.Net.Shared/Matchers/NotNullOrEmptyMatcher.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AnyOfTypes;
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Models;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// NotNullOrEmptyMatcher
|
||||
/// </summary>
|
||||
/// <seealso cref="IObjectMatcher" />
|
||||
public class NotNullOrEmptyMatcher : IObjectMatcher, IStringMatcher
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(NotNullOrEmptyMatcher);
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchBehaviour MatchBehaviour { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotNullOrEmptyMatcher"/> class.
|
||||
/// </summary>
|
||||
/// <param name="matchBehaviour">The match behaviour.</param>
|
||||
public NotNullOrEmptyMatcher(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
|
||||
{
|
||||
MatchBehaviour = matchBehaviour;
|
||||
Value = string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchResult IsMatch(object? input)
|
||||
{
|
||||
bool match;
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case string @string:
|
||||
match = !string.IsNullOrEmpty(@string);
|
||||
break;
|
||||
|
||||
case byte[] bytes:
|
||||
match = bytes.Any();
|
||||
break;
|
||||
|
||||
default:
|
||||
match = input != null;
|
||||
break;
|
||||
}
|
||||
|
||||
return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(match));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchResult IsMatch(string? input)
|
||||
{
|
||||
var match = !string.IsNullOrEmpty(input);
|
||||
|
||||
return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(match));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AnyOf<string, StringPattern>[] GetPatterns()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchOperator MatchOperator => MatchOperator.Or;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCSharpCodeArguments()
|
||||
{
|
||||
return $"new {Name}" +
|
||||
$"(" +
|
||||
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}" +
|
||||
$")";
|
||||
}
|
||||
}
|
||||
144
src/WireMock.Net.Shared/Matchers/RegexMatcher.cs
Normal file
144
src/WireMock.Net.Shared/Matchers/RegexMatcher.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AnyOfTypes;
|
||||
using JetBrains.Annotations;
|
||||
using Stef.Validation;
|
||||
using WireMock.Constants;
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Models;
|
||||
using WireMock.RegularExpressions;
|
||||
using WireMock.Util;
|
||||
|
||||
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;
|
||||
private readonly bool _useRegexExtended;
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchBehaviour MatchBehaviour { 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="useRegexExtended">Use RegexExtended (default = true).</param>
|
||||
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
|
||||
public RegexMatcher(
|
||||
[RegexPattern] AnyOf<string, StringPattern> pattern,
|
||||
bool ignoreCase = false,
|
||||
bool useRegexExtended = true,
|
||||
MatchOperator matchOperator = MatchOperator.Or) :
|
||||
this(MatchBehaviour.AcceptOnMatch, [pattern], ignoreCase, useRegexExtended, matchOperator)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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="useRegexExtended">Use RegexExtended (default = true).</param>
|
||||
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
|
||||
public RegexMatcher(
|
||||
MatchBehaviour matchBehaviour,
|
||||
[RegexPattern] AnyOf<string, StringPattern> pattern,
|
||||
bool ignoreCase = false,
|
||||
bool useRegexExtended = true,
|
||||
MatchOperator matchOperator = MatchOperator.Or) :
|
||||
this(matchBehaviour, [pattern], ignoreCase, useRegexExtended, matchOperator)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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="useRegexExtended">Use RegexExtended (default = true).</param>
|
||||
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
|
||||
public RegexMatcher(
|
||||
MatchBehaviour matchBehaviour,
|
||||
[RegexPattern] AnyOf<string, StringPattern>[] patterns,
|
||||
bool ignoreCase = false,
|
||||
bool useRegexExtended = true,
|
||||
MatchOperator matchOperator = MatchOperator.Or)
|
||||
{
|
||||
_patterns = Guard.NotNull(patterns);
|
||||
IgnoreCase = ignoreCase;
|
||||
_useRegexExtended = useRegexExtended;
|
||||
MatchBehaviour = matchBehaviour;
|
||||
MatchOperator = matchOperator;
|
||||
|
||||
var 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, RegexConstants.DefaultTimeout)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual MatchResult IsMatch(string? input)
|
||||
{
|
||||
var score = MatchScores.Mismatch;
|
||||
Exception? exception = null;
|
||||
|
||||
if (input != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
score = MatchScores.ToScore(_expressions.Select(e => e.IsMatch(input)).ToArray(), MatchOperator);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
|
||||
return new MatchResult(MatchBehaviourHelper.Convert(MatchBehaviour, score), exception);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual AnyOf<string, StringPattern>[] GetPatterns()
|
||||
{
|
||||
return _patterns;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string Name => nameof(RegexMatcher);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IgnoreCase { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MatchOperator MatchOperator { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string GetCSharpCodeArguments()
|
||||
{
|
||||
return $"new {Name}" +
|
||||
$"(" +
|
||||
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
|
||||
$"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}, " +
|
||||
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
|
||||
$"{CSharpFormatter.ToCSharpBooleanLiteral(_useRegexExtended)}, " +
|
||||
$"{MatchOperator.GetFullyQualifiedEnumValue()}" +
|
||||
$")";
|
||||
}
|
||||
}
|
||||
97
src/WireMock.Net.Shared/Matchers/WildcardMatcher.cs
Normal file
97
src/WireMock.Net.Shared/Matchers/WildcardMatcher.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AnyOfTypes;
|
||||
using Stef.Validation;
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Models;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// WildcardMatcher
|
||||
/// </summary>
|
||||
/// <seealso cref="RegexMatcher" />
|
||||
public class WildcardMatcher : RegexMatcher
|
||||
{
|
||||
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(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, 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(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="matchOperator">The <see cref="MatchOperator"/> to use. (default = "Or")</param>
|
||||
public WildcardMatcher(
|
||||
MatchBehaviour matchBehaviour,
|
||||
AnyOf<string, StringPattern>[] patterns,
|
||||
bool ignoreCase = false,
|
||||
MatchOperator matchOperator = MatchOperator.Or) : base(matchBehaviour, CreateArray(patterns), ignoreCase, true, matchOperator)
|
||||
{
|
||||
_patterns = Guard.NotNull(patterns);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AnyOf<string, StringPattern>[] GetPatterns()
|
||||
{
|
||||
return _patterns;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Name => nameof(WildcardMatcher);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetCSharpCodeArguments()
|
||||
{
|
||||
return $"new {Name}" +
|
||||
$"(" +
|
||||
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
|
||||
$"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}, " +
|
||||
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
|
||||
$"{MatchOperator.GetFullyQualifiedEnumValue()}" +
|
||||
$")";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
67
src/WireMock.Net.Shared/Models/BodyData.cs
Normal file
67
src/WireMock.Net.Shared/Models/BodyData.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WireMock.Models;
|
||||
using WireMock.Types;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// BodyData
|
||||
/// </summary>
|
||||
public class BodyData : IBodyData
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Encoding? Encoding { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? BodyAsString { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? BodyAsJson { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[]? BodyAsBytes { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool? BodyAsJsonIndented { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? BodyAsFile { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool? BodyAsFileIsCached { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BodyType? DetectedBodyType { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BodyType? DetectedBodyTypeFromContentType { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? DetectedCompression { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? IsFuncUsed { get; set; }
|
||||
|
||||
#region ProtoBuf
|
||||
/// <inheritdoc />
|
||||
public Func<IdOrTexts>? ProtoDefinition { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? ProtoBufMessageType { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBlockingQueue<string?>? SseStringQueue { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task? BodyAsSseStringTask { get; set; }
|
||||
}
|
||||
12
src/WireMock.Net.Shared/Properties/AssemblyInfo.cs
Normal file
12
src/WireMock.Net.Shared/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("WireMock.Net.Minimal, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
|
||||
[assembly: InternalsVisibleTo("WireMock.Net.MimePart, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
|
||||
[assembly: InternalsVisibleTo("WireMock.Net.Matchers.CSharpCode, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
|
||||
// [assembly: InternalsVisibleTo("WireMock.Net.StandAlone, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
|
||||
[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
|
||||
|
||||
// Needed for Moq in the UnitTest project
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||
92
src/WireMock.Net.Shared/RegularExpressions/RegexExtended.cs
Normal file
92
src/WireMock.Net.Shared/RegularExpressions/RegexExtended.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Stef.Validation;
|
||||
|
||||
namespace WireMock.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension to the <see cref="Regex"/> object, adding support for GUID tokens for matching on.
|
||||
/// </summary>
|
||||
#if !NETSTANDARD1_3
|
||||
[Serializable]
|
||||
#endif
|
||||
internal class RegexExtended : Regex
|
||||
{
|
||||
/// <inheritdoc cref="Regex"/>
|
||||
public RegexExtended(string pattern) : this(pattern, RegexOptions.None)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Regex"/>
|
||||
public RegexExtended(string pattern, RegexOptions options) :
|
||||
this(pattern, options, InfiniteMatchTimeout)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Regex"/>
|
||||
public RegexExtended(string pattern, RegexOptions options, TimeSpan matchTimeout) :
|
||||
base(ReplaceGuidPattern(pattern), options, matchTimeout)
|
||||
{
|
||||
}
|
||||
|
||||
#if !NETSTANDARD1_3 && !NET8_0_OR_GREATER
|
||||
/// <inheritdoc cref="Regex"/>
|
||||
protected RegexExtended(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) :
|
||||
base(info, context)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
// Dictionary of various Guid tokens with a corresponding regular expression pattern to use instead.
|
||||
private static readonly Dictionary<string, string> GuidTokenPatterns = new()
|
||||
{
|
||||
// Lower case format `B` Guid pattern
|
||||
{ @"\guidb", @"(\{[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\})" },
|
||||
|
||||
// Upper case format `B` Guid pattern
|
||||
{ @"\GUIDB", @"(\{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}\})" },
|
||||
|
||||
// Lower case format `D` Guid pattern
|
||||
{ @"\guidd", "([a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12})" },
|
||||
|
||||
// Upper case format `D` Guid pattern
|
||||
{ @"\GUIDD", "([A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12})" },
|
||||
|
||||
// Lower case format `N` Guid pattern
|
||||
{ @"\guidn", "([a-z0-9]{32})" },
|
||||
|
||||
// Upper case format `N` Guid pattern
|
||||
{ @"\GUIDN", "([A-Z0-9]{32})" },
|
||||
|
||||
// Lower case format `P` Guid pattern
|
||||
{ @"\guidp", @"(\([a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\))" },
|
||||
|
||||
// Upper case format `P` Guid pattern
|
||||
{ @"\GUIDP", @"(\([A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}\))" },
|
||||
|
||||
// Lower case format `X` Guid pattern
|
||||
{ @"\guidx", @"(\{0x[a-f0-9]{8},0x[a-f0-9]{4},0x[a-f0-9]{4},\{(0x[a-f0-9]{2},){7}(0x[a-f0-9]{2})\}\})" },
|
||||
|
||||
// Upper case format `X` Guid pattern
|
||||
{ @"\GUIDX", @"(\{0x[A-F0-9]{8},0x[A-F0-9]{4},0x[A-F0-9]{4},\{(0x[A-F0-9]{2},){7}(0x[A-F0-9]{2})\}\})" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all instances of valid GUID tokens with the correct regular expression to match.
|
||||
/// </summary>
|
||||
/// <param name="pattern">Pattern to replace token for.</param>
|
||||
private static string ReplaceGuidPattern(string pattern)
|
||||
{
|
||||
Guard.NotNull(pattern);
|
||||
|
||||
foreach (var tokenPattern in GuidTokenPatterns)
|
||||
{
|
||||
pattern = pattern.Replace(tokenPattern.Key, tokenPattern.Value);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace WireMock.Serialization;
|
||||
|
||||
internal static class JsonSerializationConstants
|
||||
{
|
||||
public static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new()
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
public static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new()
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Include
|
||||
};
|
||||
|
||||
public static readonly JsonSerializerSettings JsonDeserializerSettingsWithDateParsingNone = new()
|
||||
{
|
||||
DateParseHandling = DateParseHandling.None
|
||||
};
|
||||
|
||||
public static readonly JsonSerializerSettings JsonSerializerSettingsPact = new()
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
};
|
||||
}
|
||||
218
src/WireMock.Net.Shared/Util/BodyParser.cs
Normal file
218
src/WireMock.Net.Shared/Util/BodyParser.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Stef.Validation;
|
||||
using WireMock.Constants;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
internal static class BodyParser
|
||||
{
|
||||
private static readonly Encoding DefaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||
private static readonly Encoding[] SupportedBodyAsStringEncodingForMultipart = [ DefaultEncoding, Encoding.ASCII ];
|
||||
|
||||
/*
|
||||
HEAD - No defined body semantics.
|
||||
GET - No defined body semantics.
|
||||
PUT - Body supported.
|
||||
POST - Body supported.
|
||||
DELETE - No defined body semantics.
|
||||
TRACE - Body not supported.
|
||||
OPTIONS - Body supported but no semantics on usage (maybe in the future).
|
||||
CONNECT - No defined body semantics
|
||||
PATCH - Body supported.
|
||||
*/
|
||||
private static readonly IDictionary<string, bool> BodyAllowedForMethods = new Dictionary<string, bool>
|
||||
{
|
||||
{ HttpRequestMethod.HEAD, false },
|
||||
{ HttpRequestMethod.GET, false },
|
||||
{ HttpRequestMethod.PUT, true },
|
||||
{ HttpRequestMethod.POST, true },
|
||||
{ HttpRequestMethod.DELETE, true },
|
||||
{ HttpRequestMethod.TRACE, false },
|
||||
{ HttpRequestMethod.OPTIONS, true },
|
||||
{ HttpRequestMethod.CONNECT, false },
|
||||
{ HttpRequestMethod.PATCH, true }
|
||||
};
|
||||
|
||||
private static readonly IStringMatcher[] MultipartContentTypesMatchers =
|
||||
[
|
||||
new WildcardMatcher("multipart/*", true)
|
||||
];
|
||||
|
||||
private static readonly IStringMatcher[] JsonContentTypesMatchers =
|
||||
[
|
||||
new WildcardMatcher("application/json", true),
|
||||
new WildcardMatcher("application/vnd.*+json", true)
|
||||
];
|
||||
|
||||
private static readonly IStringMatcher FormUrlEncodedMatcher = new WildcardMatcher("application/x-www-form-urlencoded", true);
|
||||
|
||||
private static readonly IStringMatcher[] TextContentTypeMatchers =
|
||||
[
|
||||
new WildcardMatcher("text/*", true),
|
||||
new RegexMatcher("^application\\/(java|type)script$", true),
|
||||
new WildcardMatcher("application/*xml", true),
|
||||
FormUrlEncodedMatcher
|
||||
];
|
||||
|
||||
private static readonly IStringMatcher[] GrpcContentTypesMatchers =
|
||||
[
|
||||
new WildcardMatcher("application/grpc", true),
|
||||
new WildcardMatcher("application/grpc+proto", true)
|
||||
];
|
||||
|
||||
public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpMethods)
|
||||
{
|
||||
if (string.IsNullOrEmpty(httpMethod))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (allowBodyForAllHttpMethods)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (BodyAllowedForMethods.TryGetValue(httpMethod!.ToUpper(), out var allowed))
|
||||
{
|
||||
return allowed;
|
||||
}
|
||||
|
||||
// If we don't have any knowledge of this method, we should assume that a body *may*
|
||||
// be present, so we should parse it if it is. Therefore, if a new method is added to
|
||||
// the HTTP Method Registry, we only really need to add it to BodyAllowedForMethods if
|
||||
// we want to make it clear that a body is *not* allowed.
|
||||
return true;
|
||||
}
|
||||
|
||||
public static BodyType DetectBodyTypeFromContentType(string? contentTypeValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentTypeValue) || !MediaTypeHeaderValue.TryParse(contentTypeValue, out var contentType))
|
||||
{
|
||||
return BodyType.Bytes;
|
||||
}
|
||||
|
||||
if (FormUrlEncodedMatcher.IsMatch(contentType.MediaType).IsPerfect())
|
||||
{
|
||||
return BodyType.FormUrlEncoded;
|
||||
}
|
||||
|
||||
if (TextContentTypeMatchers.Any(matcher => matcher.IsMatch(contentType.MediaType).IsPerfect()))
|
||||
{
|
||||
return BodyType.String;
|
||||
}
|
||||
|
||||
if (JsonContentTypesMatchers.Any(matcher => matcher.IsMatch(contentType.MediaType).IsPerfect()))
|
||||
{
|
||||
return BodyType.Json;
|
||||
}
|
||||
|
||||
if (GrpcContentTypesMatchers.Any(matcher => matcher.IsMatch(contentType.MediaType).IsPerfect()))
|
||||
{
|
||||
return BodyType.ProtoBuf;
|
||||
}
|
||||
|
||||
if (MultipartContentTypesMatchers.Any(matcher => matcher.IsMatch(contentType.MediaType).IsPerfect()))
|
||||
{
|
||||
return BodyType.MultiPart;
|
||||
}
|
||||
|
||||
return BodyType.Bytes;
|
||||
}
|
||||
|
||||
public static async Task<BodyData> ParseAsync(BodyParserSettings settings)
|
||||
{
|
||||
Guard.NotNull(settings);
|
||||
|
||||
var bodyWithContentEncoding = await ReadBytesAsync(settings.Stream, settings.ContentEncoding, settings.DecompressGZipAndDeflate).ConfigureAwait(false);
|
||||
var data = new BodyData
|
||||
{
|
||||
BodyAsBytes = bodyWithContentEncoding.Bytes,
|
||||
DetectedCompression = bodyWithContentEncoding.ContentType,
|
||||
DetectedBodyType = BodyType.Bytes,
|
||||
DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(settings.ContentType)
|
||||
};
|
||||
|
||||
// In case of MultiPart: check if the BodyAsBytes is a valid UTF8 or ASCII string, in that case read as String else keep as-is
|
||||
if (data.DetectedBodyTypeFromContentType == BodyType.MultiPart)
|
||||
{
|
||||
if (BytesEncodingUtils.TryGetEncoding(data.BodyAsBytes, out var encoding) &&
|
||||
SupportedBodyAsStringEncodingForMultipart.Select(x => x.Equals(encoding)).Any())
|
||||
{
|
||||
data.BodyAsString = encoding.GetString(data.BodyAsBytes);
|
||||
data.Encoding = encoding;
|
||||
data.DetectedBodyType = BodyType.String;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Try to get the body as String, FormUrlEncoded or Json
|
||||
try
|
||||
{
|
||||
data.BodyAsString = DefaultEncoding.GetString(data.BodyAsBytes);
|
||||
data.Encoding = DefaultEncoding;
|
||||
data.DetectedBodyType = BodyType.String;
|
||||
|
||||
// If string is not null or empty, try to deserialize the string to a IDictionary<string, string>
|
||||
if (settings.DeserializeFormUrlEncoded &&
|
||||
data.DetectedBodyTypeFromContentType == BodyType.FormUrlEncoded &&
|
||||
QueryStringParser.TryParse(data.BodyAsString, false, out var nameValueCollection)
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
data.BodyAsFormUrlEncoded = nameValueCollection;
|
||||
data.DetectedBodyType = BodyType.FormUrlEncoded;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Deserialize FormUrlEncoded failed, just ignore.
|
||||
}
|
||||
}
|
||||
|
||||
// If string is not null or empty, try to deserialize the string to a JObject
|
||||
if (settings.DeserializeJson && JsonUtils.IsJson(data.BodyAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
data.BodyAsJson = JsonUtils.DeserializeObject(data.BodyAsString);
|
||||
data.DetectedBodyType = BodyType.Json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// JsonConvert failed, just ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reading as string failed, just ignore
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async Task<(string? ContentType, byte[] Bytes)> ReadBytesAsync(Stream stream, string? contentEncoding = null, bool decompressGZipAndDeflate = true)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
byte[] data = memoryStream.ToArray();
|
||||
|
||||
var type = contentEncoding?.ToLowerInvariant();
|
||||
if (decompressGZipAndDeflate && type is "gzip" or "deflate")
|
||||
{
|
||||
return (type, CompressionUtils.Decompress(type, data));
|
||||
}
|
||||
|
||||
return (null, data);
|
||||
}
|
||||
}
|
||||
38
src/WireMock.Net.Shared/Util/BodyParserSettings.cs
Normal file
38
src/WireMock.Net.Shared/Util/BodyParserSettings.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
internal class BodyParserSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The body stream to parse.
|
||||
/// </summary>
|
||||
public Stream Stream { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The (optional) content type of the body.
|
||||
/// </summary>
|
||||
public string? ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The (optional) content encoding of the body.
|
||||
/// </summary>
|
||||
public string? ContentEncoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Automatically decompress GZip and Deflate encoded content.
|
||||
/// </summary>
|
||||
public bool DecompressGZipAndDeflate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Try to deserialize the body as JSON.
|
||||
/// </summary>
|
||||
public bool DeserializeJson { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Try to deserialize the body as FormUrlEncoded.
|
||||
/// </summary>
|
||||
public bool DeserializeFormUrlEncoded { get; set; } = true;
|
||||
}
|
||||
233
src/WireMock.Net.Shared/Util/BytesEncodingUtils.cs
Normal file
233
src/WireMock.Net.Shared/Util/BytesEncodingUtils.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Some utility methods for encoding.
|
||||
/// Based on:
|
||||
/// http://utf8checker.codeplex.com
|
||||
/// https://github.com/0x53A/Mvvm/blob/master/src/Mvvm/src/Utf8Checker.cs
|
||||
///
|
||||
/// References:
|
||||
/// http://anubis.dkuug.dk/JTC1/SC2/WG2/docs/n1335
|
||||
/// http://www.cl.cam.ac.uk/~mgk25/ucs/ISO-10646-UTF-8.html
|
||||
/// http://www.unicode.org/versions/corrigendum1.html
|
||||
/// http://www.ietf.org/rfc/rfc2279.txt
|
||||
/// </summary>
|
||||
internal static class BytesEncodingUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries the get the Encoding from an array of bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <param name="encoding">The output encoding.</param>
|
||||
public static bool TryGetEncoding(byte[] bytes, [NotNullWhen(true)] out Encoding? encoding)
|
||||
{
|
||||
encoding = null;
|
||||
if (bytes.All(b => b < 80))
|
||||
{
|
||||
encoding = Encoding.ASCII;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StartsWith(bytes, [0xff, 0xfe, 0x00, 0x00]))
|
||||
{
|
||||
encoding = Encoding.UTF32;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StartsWith(bytes, [0xfe, 0xff]))
|
||||
{
|
||||
encoding = Encoding.BigEndianUnicode;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StartsWith(bytes, [0xff, 0xfe]))
|
||||
{
|
||||
encoding = Encoding.Unicode;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StartsWith(bytes, [0xef, 0xbb, 0xbf]))
|
||||
{
|
||||
encoding = Encoding.UTF8;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsUtf8(bytes, bytes.Length))
|
||||
{
|
||||
encoding = new UTF8Encoding(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool StartsWith(IEnumerable<byte> data, IReadOnlyCollection<byte> other)
|
||||
{
|
||||
byte[] arraySelf = data.Take(other.Count).ToArray();
|
||||
return other.SequenceEqual(arraySelf);
|
||||
}
|
||||
|
||||
private static bool IsUtf8(IReadOnlyList<byte> buffer, int length)
|
||||
{
|
||||
int position = 0;
|
||||
int bytes = 0;
|
||||
while (position < length)
|
||||
{
|
||||
if (!IsValid(buffer, position, length, ref bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
position += bytes;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
|
||||
private static bool IsValid(IReadOnlyList<byte> buffer, int position, int length, ref int bytes)
|
||||
{
|
||||
if (length > buffer.Count)
|
||||
{
|
||||
throw new ArgumentException("Invalid length");
|
||||
}
|
||||
|
||||
if (position > length - 1)
|
||||
{
|
||||
bytes = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
byte ch = buffer[position];
|
||||
if (ch <= 0x7F)
|
||||
{
|
||||
bytes = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch is >= 0xc2 and <= 0xdf)
|
||||
{
|
||||
if (position >= length - 2)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0xbf)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch == 0xe0)
|
||||
{
|
||||
if (position >= length - 3)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[position + 1] < 0xa0 || buffer[position + 1] > 0xbf ||
|
||||
buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch is >= 0xe1 and <= 0xef)
|
||||
{
|
||||
if (position >= length - 3)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0xbf ||
|
||||
buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch == 0xf0)
|
||||
{
|
||||
if (position >= length - 4)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[position + 1] < 0x90 || buffer[position + 1] > 0xbf ||
|
||||
buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf ||
|
||||
buffer[position + 3] < 0x80 || buffer[position + 3] > 0xbf)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch == 0xf4)
|
||||
{
|
||||
if (position >= length - 4)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0x8f ||
|
||||
buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf ||
|
||||
buffer[position + 3] < 0x80 || buffer[position + 3] > 0xbf)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch is >= 0xf1 and <= 0xf3)
|
||||
{
|
||||
if (position >= length - 4)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[position + 1] < 0x80 || buffer[position + 1] > 0xbf ||
|
||||
buffer[position + 2] < 0x80 || buffer[position + 2] > 0xbf ||
|
||||
buffer[position + 3] < 0x80 || buffer[position + 3] > 0xbf)
|
||||
{
|
||||
bytes = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#pragma warning restore S3776 // Cognitive Complexity of methods should not be too high
|
||||
185
src/WireMock.Net.Shared/Util/CSharpFormatter.cs
Normal file
185
src/WireMock.Net.Shared/Util/CSharpFormatter.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class for converting JSON to C# anonymous object definitions.
|
||||
/// </summary>
|
||||
internal static class CSharpFormatter
|
||||
{
|
||||
private const string Null = "null";
|
||||
|
||||
#region Reserved Keywords
|
||||
private static readonly HashSet<string> CSharpReservedKeywords = new(new[]
|
||||
{
|
||||
"abstract",
|
||||
"as",
|
||||
"base",
|
||||
"bool",
|
||||
"break",
|
||||
"byte",
|
||||
"case",
|
||||
"catch",
|
||||
"char",
|
||||
"checked",
|
||||
"class",
|
||||
"const",
|
||||
"continue",
|
||||
"decimal",
|
||||
"default",
|
||||
"delegate",
|
||||
"do",
|
||||
"double",
|
||||
"else",
|
||||
"enum",
|
||||
"event",
|
||||
"explicit",
|
||||
"extern",
|
||||
"false",
|
||||
"finally",
|
||||
"fixed",
|
||||
"float",
|
||||
"for",
|
||||
"foreach",
|
||||
"goto",
|
||||
"if",
|
||||
"implicit",
|
||||
"in",
|
||||
"int",
|
||||
"interface",
|
||||
"internal",
|
||||
"is",
|
||||
"lock",
|
||||
"long",
|
||||
"namespace",
|
||||
"new",
|
||||
"null",
|
||||
"object",
|
||||
"operator",
|
||||
"out",
|
||||
"override",
|
||||
"params",
|
||||
"private",
|
||||
"protected",
|
||||
"public",
|
||||
"readonly",
|
||||
"ref",
|
||||
"return",
|
||||
"sbyte",
|
||||
"sealed",
|
||||
"short",
|
||||
"sizeof",
|
||||
"stackalloc",
|
||||
"static",
|
||||
"string",
|
||||
"struct",
|
||||
"switch",
|
||||
"this",
|
||||
"throw",
|
||||
"true",
|
||||
"try",
|
||||
"typeof",
|
||||
"uint",
|
||||
"ulong",
|
||||
"unchecked",
|
||||
"unsafe",
|
||||
"ushort",
|
||||
"using",
|
||||
"virtual",
|
||||
"void",
|
||||
"volatile",
|
||||
"while"
|
||||
});
|
||||
#endregion
|
||||
|
||||
public static string ConvertToAnonymousObjectDefinition(object jsonBody, int ind = 2)
|
||||
{
|
||||
var serializedBody = JsonConvert.SerializeObject(jsonBody);
|
||||
|
||||
using var jsonReader = new JsonTextReader(new StringReader(serializedBody));
|
||||
jsonReader.DateParseHandling = DateParseHandling.None;
|
||||
|
||||
var deserializedBody = JToken.Load(jsonReader);
|
||||
return ConvertJsonToAnonymousObjectDefinition(deserializedBody, ind);
|
||||
}
|
||||
|
||||
public static string ConvertJsonToAnonymousObjectDefinition(JToken token, int ind = 0)
|
||||
{
|
||||
return token switch
|
||||
{
|
||||
JArray jArray => FormatArray(jArray, ind),
|
||||
JObject jObject => FormatObject(jObject, ind),
|
||||
JProperty jProperty => $"{FormatPropertyName(jProperty.Name)} = {ConvertJsonToAnonymousObjectDefinition(jProperty.Value, ind)}",
|
||||
JValue jValue => jValue.Type switch
|
||||
{
|
||||
JTokenType.None => Null,
|
||||
JTokenType.Integer => jValue.Value != null ? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value) : Null,
|
||||
JTokenType.Float => jValue.Value != null ? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value) : Null,
|
||||
JTokenType.String => ToCSharpStringLiteral(jValue.Value?.ToString()),
|
||||
JTokenType.Boolean => jValue.Value != null ? string.Format(CultureInfo.InvariantCulture, "{0}", jValue.Value).ToLower() : Null,
|
||||
JTokenType.Null => Null,
|
||||
JTokenType.Undefined => Null,
|
||||
JTokenType.Date when jValue.Value is DateTime dateValue => $"DateTime.Parse({ToCSharpStringLiteral(dateValue.ToString("s"))})",
|
||||
_ => $"UNHANDLED_CASE: {jValue.Type}"
|
||||
},
|
||||
_ => $"UNHANDLED_CASE: {token}"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToCSharpBooleanLiteral(bool value) => value ? "true" : "false";
|
||||
|
||||
public static string ToCSharpStringLiteral(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
if (value.Contains('\n'))
|
||||
{
|
||||
var escapedValue = value!.Replace("\"", "\"\"");
|
||||
return $"@\"{escapedValue}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
var escapedValue = value!.Replace("\"", "\\\"");
|
||||
return $"\"{escapedValue}\"";
|
||||
}
|
||||
}
|
||||
|
||||
public static string FormatPropertyName(string propertyName)
|
||||
{
|
||||
return CSharpReservedKeywords.Contains(propertyName) ? "@" + propertyName : propertyName;
|
||||
}
|
||||
|
||||
private static string FormatObject(JObject jObject, int ind)
|
||||
{
|
||||
var indStr = new string(' ', 4 * ind);
|
||||
var indStrSub = new string(' ', 4 * (ind + 1));
|
||||
var items = jObject.Properties().Select(x => ConvertJsonToAnonymousObjectDefinition(x, ind + 1));
|
||||
|
||||
return $"new\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}";
|
||||
}
|
||||
|
||||
private static string FormatArray(JArray jArray, int ind)
|
||||
{
|
||||
var hasComplexItems = jArray.FirstOrDefault() is JObject or JArray;
|
||||
var items = jArray.Select(x => ConvertJsonToAnonymousObjectDefinition(x, hasComplexItems ? ind + 1 : ind));
|
||||
if (hasComplexItems)
|
||||
{
|
||||
var indStr = new string(' ', 4 * ind);
|
||||
var indStrSub = new string(' ', 4 * (ind + 1));
|
||||
return $"new []\r\n{indStr}{{\r\n{indStrSub}{string.Join($",\r\n{indStrSub}", items)}\r\n{indStr}}}";
|
||||
}
|
||||
|
||||
return $"new [] {{ {string.Join(", ", items)} }}";
|
||||
}
|
||||
}
|
||||
56
src/WireMock.Net.Shared/Util/CompressionUtils.cs
Normal file
56
src/WireMock.Net.Shared/Util/CompressionUtils.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Some utility methods for compressing and decompressing data.
|
||||
/// </summary>
|
||||
internal static class CompressionUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Compresses the specified data using the specified content encoding.
|
||||
/// </summary>
|
||||
/// <param name="contentEncoding">The content-encoding.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>Compressed data</returns>
|
||||
public static byte[] Compress(string contentEncoding, byte[] data)
|
||||
{
|
||||
using var compressedStream = new MemoryStream();
|
||||
using var zipStream = Create(contentEncoding, compressedStream, CompressionMode.Compress);
|
||||
zipStream.Write(data, 0, data.Length);
|
||||
|
||||
#if !NETSTANDARD1_3
|
||||
zipStream.Close();
|
||||
#endif
|
||||
return compressedStream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses the specified data using the specified content encoding.
|
||||
/// </summary>
|
||||
/// <param name="contentEncoding">The content-encoding.</param>
|
||||
/// <param name="data">The compressed data.</param>
|
||||
/// <returns>Uncompressed data</returns>
|
||||
public static byte[] Decompress(string contentEncoding, byte[] data)
|
||||
{
|
||||
using var compressedStream = new MemoryStream(data);
|
||||
using var zipStream = Create(contentEncoding, compressedStream, CompressionMode.Decompress);
|
||||
using var resultStream = new MemoryStream();
|
||||
zipStream.CopyTo(resultStream);
|
||||
return resultStream.ToArray();
|
||||
}
|
||||
|
||||
private static Stream Create(string contentEncoding, Stream stream, CompressionMode mode)
|
||||
{
|
||||
return contentEncoding switch
|
||||
{
|
||||
"gzip" => new GZipStream(stream, mode),
|
||||
"deflate" => new DeflateStream(stream, mode),
|
||||
_ => throw new NotSupportedException($"ContentEncoding '{contentEncoding}' is not supported.")
|
||||
};
|
||||
}
|
||||
}
|
||||
35
src/WireMock.Net.Shared/Util/IMimeKitUtils.cs
Normal file
35
src/WireMock.Net.Shared/Util/IMimeKitUtils.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the interface for MimeKitUtils.
|
||||
/// </summary>
|
||||
public interface IMimeKitUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the MimeKit.MimeMessage from the stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream</param>
|
||||
/// <returns>MimeKit.MimeMessage</returns>
|
||||
object LoadFromStream(Stream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the MimeKit.MimeMessage from the request message.
|
||||
/// </summary>
|
||||
/// <param name="requestMessage">The request message.</param>
|
||||
/// <param name="mimeMessage">The MimeKit.MimeMessage</param>
|
||||
/// <returns><c>true</c> when parsed correctly, else <c>false</c></returns>
|
||||
bool TryGetMimeMessage(IRequestMessage requestMessage, [NotNullWhen(true)] out object? mimeMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the body parts from the MimeKit.MimeMessage.
|
||||
/// </summary>
|
||||
/// <param name="mimeMessage">The MimeKit.MimeMessage.</param>
|
||||
/// <returns>A list of MimeParts.</returns>
|
||||
IReadOnlyList<object> GetBodyParts(object mimeMessage);
|
||||
}
|
||||
137
src/WireMock.Net.Shared/Util/JsonUtils.cs
Normal file
137
src/WireMock.Net.Shared/Util/JsonUtils.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WireMock.Serialization;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
internal static class JsonUtils
|
||||
{
|
||||
public static bool IsJson(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = value!.Trim();
|
||||
|
||||
return (value.StartsWith("{") && value.EndsWith("}")) || (value.StartsWith("[") && value.EndsWith("]"));
|
||||
}
|
||||
|
||||
public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
if (!IsJson(strInput))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to convert this string into a JToken
|
||||
value = JObject.Parse(strInput!);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Serialize(object value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues);
|
||||
}
|
||||
|
||||
public static byte[] SerializeAsPactFile(object value)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsPact);
|
||||
return Encoding.UTF8.GetBytes(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a Newtonsoft.Json.Linq.JObject from a string that contains JSON.
|
||||
/// Using : DateParseHandling = DateParseHandling.None
|
||||
/// </summary>
|
||||
/// <param name="json">A System.String that contains JSON.</param>
|
||||
/// <returns>A Newtonsoft.Json.Linq.JToken populated from the string that contains JSON.</returns>
|
||||
public static JToken Parse(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<JToken>(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the JSON to a .NET object.
|
||||
/// Using : DateParseHandling = DateParseHandling.None
|
||||
/// </summary>
|
||||
/// <param name="json">A System.String that contains JSON.</param>
|
||||
/// <returns>The deserialized object from the JSON string.</returns>
|
||||
public static object DeserializeObject(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the JSON to the specified .NET type.
|
||||
/// Using : DateParseHandling = DateParseHandling.None
|
||||
/// </summary>
|
||||
/// <param name="json">A System.String that contains JSON.</param>
|
||||
/// <returns>The deserialized object from the JSON string.</returns>
|
||||
public static T DeserializeObject<T>(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!;
|
||||
}
|
||||
|
||||
public static T? TryDeserializeObject<T>(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static T ParseJTokenToObject<T>(object? value)
|
||||
{
|
||||
if (value != null && value.GetType() == typeof(T))
|
||||
{
|
||||
return (T)value;
|
||||
}
|
||||
|
||||
return value switch
|
||||
{
|
||||
JToken tokenValue => tokenValue.ToObject<T>()!,
|
||||
|
||||
_ => throw new NotSupportedException($"Unable to convert value to {typeof(T)}.")
|
||||
};
|
||||
}
|
||||
|
||||
public static JToken ConvertValueToJToken(object value)
|
||||
{
|
||||
// Check if JToken, string, IEnumerable or object
|
||||
switch (value)
|
||||
{
|
||||
case JToken tokenValue:
|
||||
return tokenValue;
|
||||
|
||||
case string stringValue:
|
||||
return Parse(stringValue);
|
||||
|
||||
case IEnumerable enumerableValue:
|
||||
return JArray.FromObject(enumerableValue);
|
||||
|
||||
default:
|
||||
return JObject.FromObject(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/WireMock.Net.Shared/Util/MappingConverterUtils.cs
Normal file
36
src/WireMock.Net.Shared/Util/MappingConverterUtils.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AnyOfTypes;
|
||||
using WireMock.Extensions;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Models;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Some MappingConverter utility methods.
|
||||
/// </summary>
|
||||
internal static class MappingConverterUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a list of matchers to C# code arguments.
|
||||
/// </summary>
|
||||
/// <param name="matchers">A list of matchers.</param>
|
||||
/// <returns>The C# code arguments as string.</returns>
|
||||
public static string ToCSharpCodeArguments(IReadOnlyList<IMatcher> matchers)
|
||||
{
|
||||
return string.Join(", ", matchers.Select(m => m.GetCSharpCodeArguments()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a list of patterns to C# code arguments.
|
||||
/// </summary>
|
||||
/// <param name="patterns">The patterns.</param>
|
||||
/// <returns>The C# code arguments as string.</returns>
|
||||
public static string ToCSharpCodeArguments(AnyOf<string, StringPattern>[] patterns)
|
||||
{
|
||||
return string.Join(", ", patterns.Select(p => CSharpFormatter.ToCSharpStringLiteral(p.GetPattern())));
|
||||
}
|
||||
}
|
||||
85
src/WireMock.Net.Shared/Util/QueryStringParser.cs
Normal file
85
src/WireMock.Net.Shared/Util/QueryStringParser.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Util;
|
||||
|
||||
/// <summary>
|
||||
/// QueryStringParser (based on https://stackoverflow.com/questions/659887/get-url-parameters-from-a-string-in-net)
|
||||
/// </summary>
|
||||
internal static class QueryStringParser
|
||||
{
|
||||
private static readonly Dictionary<string, WireMockList<string>> Empty = new();
|
||||
|
||||
public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary<string, string>? nameValueCollection)
|
||||
{
|
||||
if (queryString == null)
|
||||
{
|
||||
nameValueCollection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = queryString
|
||||
.Split(["&"], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(parameter => parameter.Split('='))
|
||||
.Distinct();
|
||||
|
||||
nameValueCollection = caseIgnore ? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) : new Dictionary<string, string>();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.Length == 2)
|
||||
{
|
||||
nameValueCollection.Add(part[0], WebUtility.UrlDecode(part[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IDictionary<string, WireMockList<string>> Parse(string? queryString, QueryParameterMultipleValueSupport? support = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(queryString))
|
||||
{
|
||||
return Empty;
|
||||
}
|
||||
|
||||
var queryParameterMultipleValueSupport = support ?? QueryParameterMultipleValueSupport.All;
|
||||
|
||||
var splitOn = new List<string>();
|
||||
if (queryParameterMultipleValueSupport.HasFlag(QueryParameterMultipleValueSupport.Ampersand))
|
||||
{
|
||||
splitOn.Add("&"); // Support "?key=value&key=anotherValue"
|
||||
}
|
||||
if (queryParameterMultipleValueSupport.HasFlag(QueryParameterMultipleValueSupport.SemiColon))
|
||||
{
|
||||
splitOn.Add(";"); // Support "?key=value;key=anotherValue"
|
||||
}
|
||||
|
||||
return queryString!.TrimStart('?')
|
||||
.Split(splitOn.ToArray(), StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(parameter => new { hasEqualSign = parameter.Contains('='), parts = parameter.Split(['='], 2, StringSplitOptions.RemoveEmptyEntries) })
|
||||
.GroupBy(x => x.parts[0], y => JoinParts(y.hasEqualSign, y.parts))
|
||||
.ToDictionary
|
||||
(
|
||||
grouping => grouping.Key,
|
||||
grouping => new WireMockList<string>(grouping.SelectMany(x => x).Select(WebUtility.UrlDecode).OfType<string>())
|
||||
);
|
||||
|
||||
string[] JoinParts(bool hasEqualSign, string[] parts)
|
||||
{
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
return queryParameterMultipleValueSupport.HasFlag(QueryParameterMultipleValueSupport.Comma) ?
|
||||
parts[1].Split([","], StringSplitOptions.RemoveEmptyEntries) : // Support "?key=1,2"
|
||||
[parts[1]];
|
||||
}
|
||||
|
||||
return hasEqualSign ? [string.Empty] : []; // Return empty string if equal sign with no value (#1247)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/WireMock.Net.Shared/WireMock.Net.Shared.csproj
Normal file
45
src/WireMock.Net.Shared/WireMock.Net.Shared.csproj
Normal file
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>Shared interfaces, models, enumerations and types.</Description>
|
||||
<Authors>Stef Heyenrath</Authors>
|
||||
<TargetFrameworks>net451;net452;net46;net461;netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>tdd;mock;http;wiremock;test;server;shared</PackageTags>
|
||||
<RootNamespace>WireMock</RootNamespace>
|
||||
<ProjectGuid>{D3804228-91F4-4502-9595-39584E5A0177}</ProjectGuid>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<!--<DelaySign>true</DelaySign>-->
|
||||
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- https://github.com/aspnet/RoslynCodeDomProvider/issues/51 -->
|
||||
<!-- This is needed else we cannot build net452 in Azure DevOps Pipeline -->
|
||||
<!--<Target Name="CheckIfShouldKillVBCSCompiler" />-->
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug - Sonar'">
|
||||
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<!--<PathMap>$(MSBuildProjectDirectory)=/</PathMap>-->
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PolySharp" Version="1.15.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Stef.Validation" Version="0.1.1" />
|
||||
<PackageReference Include="AnyOf" Version="0.4.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user