mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-05-20 23:17:04 +02:00
...
This commit is contained in:
@@ -10,6 +10,7 @@ using WireMock.Constants;
|
||||
using WireMock.Logging;
|
||||
using WireMock.Models;
|
||||
using WireMock.Types;
|
||||
using WireMock.Transformers;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Settings;
|
||||
@@ -74,6 +75,8 @@ public static class WireMockServerSettingsParser
|
||||
WatchStaticMappingsInSubdirectories = parser.GetBoolValue(nameof(WireMockServerSettings.WatchStaticMappingsInSubdirectories)),
|
||||
};
|
||||
|
||||
settings.DefaultJsonBodyTransformer = new NewtonsoftJsonBodyTransformer(settings);
|
||||
|
||||
#if USE_ASPNETCORE
|
||||
settings.CorsPolicyOptions = parser.GetEnumValue(nameof(WireMockServerSettings.CorsPolicyOptions), CorsPolicyOptions.None);
|
||||
settings.ClientCertificateMode = parser.GetEnumValue(nameof(WireMockServerSettings.ClientCertificateMode), ClientCertificateMode.NoCertificate);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using HandlebarsDotNet;
|
||||
using WireMock.Transformers;
|
||||
|
||||
namespace WireMock.Transformers.Handlebars;
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using WireMock.Handlers;
|
||||
|
||||
namespace WireMock.Transformers;
|
||||
|
||||
internal interface ITransformerContext
|
||||
{
|
||||
IFileSystemHandler FileSystemHandler { get; }
|
||||
|
||||
string ParseAndRender(string text, object model);
|
||||
|
||||
object? ParseAndEvaluate(string text, object model);
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using HandlebarsDotNet.Helpers.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Stef.Validation;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
@@ -14,17 +9,13 @@ namespace WireMock.Transformers;
|
||||
|
||||
internal class Transformer : ITransformer
|
||||
{
|
||||
private readonly JsonSerializer _jsonSerializer;
|
||||
private readonly IJsonBodyTransformer _jsonBodyTransformer;
|
||||
private readonly ITransformerContextFactory _factory;
|
||||
|
||||
public Transformer(WireMockServerSettings settings, ITransformerContextFactory factory)
|
||||
{
|
||||
_factory = Guard.NotNull(factory);
|
||||
|
||||
_jsonSerializer = new JsonSerializer
|
||||
{
|
||||
Culture = Guard.NotNull(settings).Culture
|
||||
};
|
||||
_jsonBodyTransformer = Guard.NotNull(settings).DefaultJsonBodyTransformer;
|
||||
}
|
||||
|
||||
public IBodyData? TransformBody(
|
||||
@@ -121,13 +112,17 @@ internal class Transformer : ITransformer
|
||||
});
|
||||
}
|
||||
|
||||
private IBodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile)
|
||||
private BodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile)
|
||||
{
|
||||
switch (original.DetectedBodyType)
|
||||
{
|
||||
case BodyType.Json:
|
||||
case BodyType.ProtoBuf:
|
||||
return TransformBodyAsJson(transformerContext, options, model, original);
|
||||
return _jsonBodyTransformer.TransformBodyAsJson(
|
||||
transformerContext,
|
||||
options,
|
||||
model,
|
||||
original);
|
||||
|
||||
case BodyType.File:
|
||||
return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile);
|
||||
@@ -159,185 +154,7 @@ internal class Transformer : ITransformer
|
||||
return newHeaders;
|
||||
}
|
||||
|
||||
private IBodyData TransformBodyAsJson(ITransformerContext transformerContext, ReplaceNodeOptions options, object model, IBodyData original)
|
||||
{
|
||||
JToken? jToken = null;
|
||||
switch (original.BodyAsJson)
|
||||
{
|
||||
case JObject bodyAsJObject:
|
||||
jToken = bodyAsJObject.DeepClone();
|
||||
WalkNode(transformerContext, options, jToken, model);
|
||||
break;
|
||||
|
||||
case JArray bodyAsJArray:
|
||||
jToken = bodyAsJArray.DeepClone();
|
||||
WalkNode(transformerContext, options, jToken, model);
|
||||
break;
|
||||
|
||||
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
|
||||
jToken = JArray.FromObject(bodyAsEnumerable, _jsonSerializer);
|
||||
WalkNode(transformerContext, options, jToken, model);
|
||||
break;
|
||||
|
||||
case string bodyAsString:
|
||||
jToken = ReplaceSingleNode(transformerContext, options, bodyAsString, model);
|
||||
break;
|
||||
|
||||
case not null:
|
||||
jToken = JObject.FromObject(original.BodyAsJson, _jsonSerializer);
|
||||
WalkNode(transformerContext, options, jToken, model);
|
||||
break;
|
||||
}
|
||||
|
||||
return new BodyData
|
||||
{
|
||||
Encoding = original.Encoding,
|
||||
DetectedBodyType = original.DetectedBodyType,
|
||||
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
|
||||
ProtoDefinition = original.ProtoDefinition,
|
||||
ProtoBufMessageType = original.ProtoBufMessageType,
|
||||
BodyAsJson = jToken
|
||||
};
|
||||
}
|
||||
|
||||
private JToken ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model)
|
||||
{
|
||||
var transformedString = transformerContext.ParseAndRender(stringValue, model);
|
||||
|
||||
if (!string.Equals(stringValue, transformedString))
|
||||
{
|
||||
const string property = "_";
|
||||
JObject dummy = JObject.Parse($"{{ \"{property}\": null }}");
|
||||
if (dummy[property] == null)
|
||||
{
|
||||
// TODO: check if just returning null is fine
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
JToken node = dummy[property]!;
|
||||
|
||||
ReplaceNodeValue(options, node, transformedString);
|
||||
|
||||
return dummy[property]!;
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
private void WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JToken node, object model)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case JTokenType.Object:
|
||||
// In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions.
|
||||
foreach (var child in node.Children<JProperty>().ToArray())
|
||||
{
|
||||
WalkNode(transformerContext, options, child.Value, model);
|
||||
}
|
||||
break;
|
||||
|
||||
case JTokenType.Array:
|
||||
// In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions.
|
||||
foreach (var child in node.Children().ToArray())
|
||||
{
|
||||
WalkNode(transformerContext, options, child, model);
|
||||
}
|
||||
break;
|
||||
|
||||
case JTokenType.String:
|
||||
// In case of string, try to transform the value.
|
||||
var stringValue = node.Value<string>();
|
||||
if (string.IsNullOrEmpty(stringValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
|
||||
if (!Equals(stringValue, transformed))
|
||||
{
|
||||
ReplaceNodeValue(options, node, transformed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedParameter.Local
|
||||
private void ReplaceNodeValue(ReplaceNodeOptions options, JToken node, object? transformedValue)
|
||||
{
|
||||
switch (transformedValue)
|
||||
{
|
||||
case JValue jValue:
|
||||
node.Replace(jValue);
|
||||
return;
|
||||
|
||||
case string transformedString:
|
||||
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
|
||||
if (isConvertedFromString)
|
||||
{
|
||||
node.Replace(JToken.FromObject(convertedValueFromString, _jsonSerializer));
|
||||
}
|
||||
else
|
||||
{
|
||||
node.Replace(ParseAsJObject(transformedString));
|
||||
}
|
||||
break;
|
||||
|
||||
case WireMockList<string> strings:
|
||||
switch (strings.Count)
|
||||
{
|
||||
case 1:
|
||||
node.Replace(ParseAsJObject(strings[0]));
|
||||
return;
|
||||
|
||||
case > 1:
|
||||
node.Replace(JToken.FromObject(strings.ToArray(), _jsonSerializer));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case { }:
|
||||
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
|
||||
if (isConverted)
|
||||
{
|
||||
node.Replace(JToken.FromObject(convertedValue, _jsonSerializer));
|
||||
}
|
||||
return;
|
||||
|
||||
default: // It's null, skip it. Maybe remove it ?
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static JToken ParseAsJObject(string stringValue)
|
||||
{
|
||||
return JsonUtils.TryParseAsJObject(stringValue, out var parsedAsjObject) ? parsedAsjObject : stringValue;
|
||||
}
|
||||
|
||||
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
|
||||
{
|
||||
var valueAsString = value as string;
|
||||
|
||||
if (options == ReplaceNodeOptions.Evaluate)
|
||||
{
|
||||
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
|
||||
{
|
||||
return (true, decoded);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
|
||||
if (valueAsString != null)
|
||||
{
|
||||
return WrappedString.TryDecode(valueAsString, out var decoded) ?
|
||||
(true, decoded) :
|
||||
StringUtils.TryConvertToKnownType(valueAsString);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
|
||||
private static IBodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original)
|
||||
private static BodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original)
|
||||
{
|
||||
return new BodyData
|
||||
{
|
||||
@@ -348,7 +165,7 @@ internal class Transformer : ITransformer
|
||||
};
|
||||
}
|
||||
|
||||
private static IBodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile)
|
||||
private static BodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile)
|
||||
{
|
||||
var transformedBodyAsFilename = transformerContext.ParseAndRender(original.BodyAsFile!, model);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using WireMock.Logging;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Models;
|
||||
using WireMock.RegularExpressions;
|
||||
using WireMock.Transformers;
|
||||
using WireMock.Types;
|
||||
|
||||
namespace WireMock.Settings;
|
||||
@@ -362,11 +363,31 @@ public class WireMockServerSettings
|
||||
/// Default is <see cref="NewtonsoftJsonConverter"/>.
|
||||
/// </remarks>
|
||||
[PublicAPI]
|
||||
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter();
|
||||
public IJsonConverter DefaultJsonSerializer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default JSON body transformer used for template-based JSON body transformations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Set this property to provide a custom implementation for transforming JSON and ProtoBuf body content.
|
||||
/// Default is <see cref="NewtonsoftJsonBodyTransformer"/>.
|
||||
/// </remarks>
|
||||
[PublicAPI]
|
||||
[JsonIgnore]
|
||||
public IJsonBodyTransformer DefaultJsonBodyTransformer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket settings.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public WebSocketSettings? WebSocketSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WireMockServerSettings"/> class.
|
||||
/// </summary>
|
||||
public WireMockServerSettings()
|
||||
{
|
||||
DefaultJsonSerializer = new NewtonsoftJsonConverter();
|
||||
DefaultJsonBodyTransformer = new NewtonsoftJsonBodyTransformer(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for transforming JSON-like body data using a transformer context.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public interface IJsonBodyTransformer
|
||||
{
|
||||
/// <summary>
|
||||
/// Transforms the JSON body using the provided transformer context and model.
|
||||
/// </summary>
|
||||
/// <param name="transformerContext">The transformer context used to render and evaluate template values.</param>
|
||||
/// <param name="options">The JSON node replacement behavior to apply during transformation.</param>
|
||||
/// <param name="model">The model used when rendering or evaluating template values.</param>
|
||||
/// <param name="original">The original body data to transform.</param>
|
||||
/// <returns>The transformed JSON body data.</returns>
|
||||
BodyData TransformBodyAsJson(
|
||||
ITransformerContext transformerContext,
|
||||
ReplaceNodeOptions options,
|
||||
object model,
|
||||
IBodyData original);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using WireMock.Handlers;
|
||||
|
||||
namespace WireMock.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the transformer context used to render and evaluate templates during response transformation.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public interface ITransformerContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file system handler used by the current transformer context.
|
||||
/// </summary>
|
||||
IFileSystemHandler FileSystemHandler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Renders the specified template text using the supplied model.
|
||||
/// </summary>
|
||||
/// <param name="text">The template text to render.</param>
|
||||
/// <param name="model">The model used during rendering.</param>
|
||||
/// <returns>The rendered text.</returns>
|
||||
string ParseAndRender(string text, object model);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the specified template text using the supplied model.
|
||||
/// </summary>
|
||||
/// <param name="text">The template text to evaluate.</param>
|
||||
/// <param name="model">The model used during evaluation.</param>
|
||||
/// <returns>The evaluated value.</returns>
|
||||
object? ParseAndEvaluate(string text, object model);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections;
|
||||
using HandlebarsDotNet.Helpers.Models;
|
||||
using JetBrains.Annotations;
|
||||
using JsonConverter.Abstractions;
|
||||
using JsonConverter.Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// Default JSON body transformer implementation based on Newtonsoft.Json.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="NewtonsoftJsonBodyTransformer"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="settings">The server settings used to configure JSON transformation behavior.</param>
|
||||
[PublicAPI]
|
||||
public class NewtonsoftJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer
|
||||
{
|
||||
private readonly IJsonConverter _jsonConverter = new NewtonsoftJsonConverter();
|
||||
|
||||
/// <inheritdoc />
|
||||
public BodyData TransformBodyAsJson(
|
||||
ITransformerContext transformerContext,
|
||||
ReplaceNodeOptions options,
|
||||
object model,
|
||||
IBodyData original)
|
||||
{
|
||||
var jsonSerializer = new JsonSerializer
|
||||
{
|
||||
Culture = settings.Culture
|
||||
};
|
||||
|
||||
JToken? jToken = null;
|
||||
switch (original.BodyAsJson)
|
||||
{
|
||||
case JObject bodyAsJObject:
|
||||
jToken = bodyAsJObject.DeepClone();
|
||||
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
|
||||
break;
|
||||
|
||||
case JArray bodyAsJArray:
|
||||
jToken = bodyAsJArray.DeepClone();
|
||||
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
|
||||
break;
|
||||
|
||||
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
|
||||
jToken = JArray.FromObject(bodyAsEnumerable, jsonSerializer);
|
||||
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
|
||||
break;
|
||||
|
||||
case string bodyAsString:
|
||||
jToken = ReplaceSingleNode(transformerContext, jsonSerializer, options, bodyAsString, model);
|
||||
break;
|
||||
|
||||
case not null:
|
||||
jToken = JObject.FromObject(original.BodyAsJson, jsonSerializer);
|
||||
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
|
||||
break;
|
||||
}
|
||||
|
||||
return new BodyData
|
||||
{
|
||||
Encoding = original.Encoding,
|
||||
DetectedBodyType = original.DetectedBodyType,
|
||||
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
|
||||
ProtoDefinition = original.ProtoDefinition,
|
||||
ProtoBufMessageType = original.ProtoBufMessageType,
|
||||
BodyAsJson = jToken
|
||||
};
|
||||
}
|
||||
|
||||
private JToken ParseAsJObject(string stringValue)
|
||||
{
|
||||
if (_jsonConverter.IsValidJson(stringValue))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to convert this string into a JObject
|
||||
return JObject.Parse(stringValue!);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
private JToken ReplaceSingleNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, string stringValue, object model)
|
||||
{
|
||||
var transformedString = transformerContext.ParseAndRender(stringValue, model);
|
||||
|
||||
if (!string.Equals(stringValue, transformedString))
|
||||
{
|
||||
const string property = "_";
|
||||
JObject dummy = JObject.Parse($"{{ \"{property}\": null }}");
|
||||
if (dummy[property] == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
JToken node = dummy[property]!;
|
||||
|
||||
ReplaceNodeValue(jsonSerializer, options, node, transformedString);
|
||||
|
||||
return dummy[property]!;
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
private void WalkNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object model)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case JTokenType.Object:
|
||||
foreach (var child in node.Children<JProperty>().ToArray())
|
||||
{
|
||||
WalkNode(transformerContext, jsonSerializer, options, child.Value, model);
|
||||
}
|
||||
break;
|
||||
|
||||
case JTokenType.Array:
|
||||
foreach (var child in node.Children().ToArray())
|
||||
{
|
||||
WalkNode(transformerContext, jsonSerializer, options, child, model);
|
||||
}
|
||||
break;
|
||||
|
||||
case JTokenType.String:
|
||||
var stringValue = node.Value<string>();
|
||||
if (string.IsNullOrEmpty(stringValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
|
||||
if (!Equals(stringValue, transformed))
|
||||
{
|
||||
ReplaceNodeValue(jsonSerializer, options, node, transformed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplaceNodeValue(JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object? transformedValue)
|
||||
{
|
||||
switch (transformedValue)
|
||||
{
|
||||
case JValue jValue:
|
||||
node.Replace(jValue);
|
||||
return;
|
||||
|
||||
case string transformedString:
|
||||
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
|
||||
if (isConvertedFromString)
|
||||
{
|
||||
node.Replace(JToken.FromObject(convertedValueFromString, jsonSerializer));
|
||||
}
|
||||
else
|
||||
{
|
||||
node.Replace(ParseAsJObject(transformedString));
|
||||
}
|
||||
break;
|
||||
|
||||
case WireMockList<string> strings:
|
||||
switch (strings.Count)
|
||||
{
|
||||
case 1:
|
||||
node.Replace(ParseAsJObject(strings[0]));
|
||||
return;
|
||||
|
||||
case > 1:
|
||||
node.Replace(JToken.FromObject(strings.ToArray(), jsonSerializer));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case { }:
|
||||
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
|
||||
if (isConverted)
|
||||
{
|
||||
node.Replace(JToken.FromObject(convertedValue, jsonSerializer));
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
|
||||
{
|
||||
var valueAsString = value as string;
|
||||
|
||||
if (options == ReplaceNodeOptions.Evaluate)
|
||||
{
|
||||
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
|
||||
{
|
||||
return (true, decoded);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
|
||||
if (valueAsString != null)
|
||||
{
|
||||
return WrappedString.TryDecode(valueAsString, out var decoded)
|
||||
? (true, decoded)
|
||||
: TryConvertToKnownType(valueAsString);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
|
||||
internal static (bool IsConverted, object ConvertedValue) TryConvertToKnownType(string value)
|
||||
{
|
||||
if (bool.TryParse(value, out var boolResult))
|
||||
{
|
||||
return (true, boolResult);
|
||||
}
|
||||
|
||||
if (int.TryParse(value, out var intResult))
|
||||
{
|
||||
return (true, intResult);
|
||||
}
|
||||
|
||||
if (long.TryParse(value, out var longResult))
|
||||
{
|
||||
return (true, longResult);
|
||||
}
|
||||
|
||||
if (double.TryParse(value, out var doubleResult))
|
||||
{
|
||||
return (true, doubleResult);
|
||||
}
|
||||
|
||||
if (Guid.TryParseExact(value, "D", out var guidResult))
|
||||
{
|
||||
return (true, guidResult);
|
||||
}
|
||||
|
||||
if (TimeSpan.TryParse(value, out var timeSpanResult))
|
||||
{
|
||||
return (true, timeSpanResult);
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(value, out var dateTimeResult))
|
||||
{
|
||||
return (true, dateTimeResult);
|
||||
}
|
||||
|
||||
if ((value.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("ftps://", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
|
||||
Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uriResult))
|
||||
{
|
||||
return (true, uriResult);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Collections;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using HandlebarsDotNet.Helpers.Models;
|
||||
using JetBrains.Annotations;
|
||||
using JsonConverter.Abstractions;
|
||||
using JsonConverter.System.Text.Json;
|
||||
using WireMock.Settings;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Transformers;
|
||||
|
||||
/// <summary>
|
||||
/// JSON body transformer implementation based on System.Text.Json.
|
||||
/// </summary>
|
||||
/// <param name="settings">The server settings used to configure JSON transformation behavior.</param>
|
||||
[PublicAPI]
|
||||
public class SystemTextJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer
|
||||
{
|
||||
private readonly IJsonConverter _jsonConverter = new SystemTextJsonConverter();
|
||||
|
||||
/// <inheritdoc />
|
||||
public BodyData TransformBodyAsJson(
|
||||
ITransformerContext transformerContext,
|
||||
ReplaceNodeOptions options,
|
||||
object model,
|
||||
IBodyData original)
|
||||
{
|
||||
JsonNode? jsonNode = null;
|
||||
switch (original.BodyAsJson)
|
||||
{
|
||||
case JsonObject bodyAsJsonObject:
|
||||
jsonNode = CloneNode(bodyAsJsonObject);
|
||||
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
|
||||
break;
|
||||
|
||||
case JsonArray bodyAsJsonArray:
|
||||
jsonNode = CloneNode(bodyAsJsonArray);
|
||||
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
|
||||
break;
|
||||
|
||||
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
|
||||
jsonNode = JsonSerializer.SerializeToNode(bodyAsEnumerable);
|
||||
if (jsonNode != null)
|
||||
{
|
||||
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
|
||||
}
|
||||
break;
|
||||
|
||||
case string bodyAsString:
|
||||
jsonNode = ReplaceSingleNode(transformerContext, options, bodyAsString, model);
|
||||
break;
|
||||
|
||||
case not null:
|
||||
jsonNode = JsonSerializer.SerializeToNode(original.BodyAsJson);
|
||||
if (jsonNode != null)
|
||||
{
|
||||
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return new BodyData
|
||||
{
|
||||
Encoding = original.Encoding,
|
||||
DetectedBodyType = original.DetectedBodyType,
|
||||
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
|
||||
ProtoDefinition = original.ProtoDefinition,
|
||||
ProtoBufMessageType = original.ProtoBufMessageType,
|
||||
BodyAsJson = jsonNode
|
||||
};
|
||||
}
|
||||
|
||||
private JsonNode ParseAsJsonObject(string stringValue)
|
||||
{
|
||||
if (_jsonConverter.IsValidJson(stringValue))
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsed = JsonNode.Parse(stringValue);
|
||||
if (parsed is JsonObject)
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore and return as string.
|
||||
}
|
||||
}
|
||||
|
||||
return JsonValue.Create(stringValue)!;
|
||||
}
|
||||
|
||||
private JsonNode? ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model)
|
||||
{
|
||||
var transformedString = transformerContext.ParseAndRender(stringValue, model);
|
||||
|
||||
if (!string.Equals(stringValue, transformedString))
|
||||
{
|
||||
return ReplaceNodeValue(options, transformedString);
|
||||
}
|
||||
|
||||
return JsonValue.Create(stringValue);
|
||||
}
|
||||
|
||||
private JsonNode? WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JsonNode? node, object model)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case JsonObject jsonObject:
|
||||
foreach (var property in jsonObject.ToArray())
|
||||
{
|
||||
jsonObject[property.Key] = WalkNode(transformerContext, options, property.Value, model);
|
||||
}
|
||||
return jsonObject;
|
||||
|
||||
case JsonArray jsonArray:
|
||||
for (var i = 0; i < jsonArray.Count; i++)
|
||||
{
|
||||
jsonArray[i] = WalkNode(transformerContext, options, jsonArray[i], model);
|
||||
}
|
||||
return jsonArray;
|
||||
|
||||
case JsonValue jsonValue when jsonValue.TryGetValue<string>(out var stringValue):
|
||||
if (string.IsNullOrEmpty(stringValue))
|
||||
{
|
||||
return jsonValue;
|
||||
}
|
||||
|
||||
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
|
||||
return !Equals(stringValue, transformed) ? ReplaceNodeValue(options, transformed) ?? jsonValue : jsonValue;
|
||||
|
||||
default:
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode? ReplaceNodeValue(ReplaceNodeOptions options, object? transformedValue)
|
||||
{
|
||||
switch (transformedValue)
|
||||
{
|
||||
case JsonNode jsonNode:
|
||||
return CloneNode(jsonNode);
|
||||
|
||||
case string transformedString:
|
||||
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
|
||||
return isConvertedFromString
|
||||
? JsonSerializer.SerializeToNode(convertedValueFromString)
|
||||
: ParseAsJsonObject(transformedString);
|
||||
|
||||
case WireMockList<string> strings:
|
||||
switch (strings.Count)
|
||||
{
|
||||
case 1:
|
||||
return ParseAsJsonObject(strings[0]);
|
||||
|
||||
case > 1:
|
||||
return JsonSerializer.SerializeToNode(strings.ToArray());
|
||||
}
|
||||
break;
|
||||
|
||||
case { }:
|
||||
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
|
||||
if (isConverted)
|
||||
{
|
||||
return JsonSerializer.SerializeToNode(convertedValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonNode? CloneNode(JsonNode? node)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
return node?.DeepClone();
|
||||
#else
|
||||
return node == null ? null : JsonNode.Parse(node.ToJsonString());
|
||||
#endif
|
||||
}
|
||||
|
||||
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
|
||||
{
|
||||
var valueAsString = value as string;
|
||||
|
||||
if (options == ReplaceNodeOptions.Evaluate)
|
||||
{
|
||||
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
|
||||
{
|
||||
return (true, decoded);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
|
||||
if (valueAsString != null)
|
||||
{
|
||||
return WrappedString.TryDecode(valueAsString, out var decoded)
|
||||
? (true, decoded)
|
||||
: NewtonsoftJsonBodyTransformer.TryConvertToKnownType(valueAsString);
|
||||
}
|
||||
|
||||
return (false, value);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
<PackageReference Include="AnyOf" Version="0.5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.10.0" />
|
||||
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user