diff --git a/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj b/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj index 3dc9ff10..32d059a3 100644 --- a/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj +++ b/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs index 6fe7dc9c..d7932f4f 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using WireMock.Models; namespace WireMock.Admin.Mappings; @@ -84,4 +85,13 @@ public class MappingModel /// Fire and forget for webhooks. /// public bool? UseWebhooksFireAndForget { get; set; } + + /// + /// Data Object which can be used when WithTransformer is used. + /// e.g. lookup an path in this object using + /// + /// lookup data "1" + /// + /// + public object? Data { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs index 40609b7f..d0ff2309 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs @@ -1,121 +1,120 @@ using System.Collections.Generic; -namespace WireMock.Admin.Mappings +namespace WireMock.Admin.Mappings; + +/// +/// ResponseModel +/// +[FluentBuilder.AutoGenerateBuilder] +public class ResponseModel { /// - /// ResponseModel + /// Gets or sets the HTTP status. /// - [FluentBuilder.AutoGenerateBuilder] - public class ResponseModel - { - /// - /// Gets or sets the HTTP status. - /// - public object? StatusCode { get; set; } + public object? StatusCode { get; set; } - /// - /// Gets or sets the body destination (SameAsSource, String or Bytes). - /// - public string? BodyDestination { get; set; } + /// + /// Gets or sets the body destination (SameAsSource, String or Bytes). + /// + public string? BodyDestination { get; set; } - /// - /// Gets or sets the body. - /// - public string? Body { get; set; } + /// + /// Gets or sets the body. + /// + public string? Body { get; set; } - /// - /// Gets or sets the body (as JSON object). - /// - public object? BodyAsJson { get; set; } + /// + /// Gets or sets the body (as JSON object). + /// + public object? BodyAsJson { get; set; } - /// - /// Gets or sets a value indicating whether child objects to be indented according to the Newtonsoft.Json.JsonTextWriter.Indentation and Newtonsoft.Json.JsonTextWriter.IndentChar settings. - /// - public bool? BodyAsJsonIndented { get; set; } + /// + /// Gets or sets a value indicating whether child objects to be indented according to the Newtonsoft.Json.JsonTextWriter.Indentation and Newtonsoft.Json.JsonTextWriter.IndentChar settings. + /// + public bool? BodyAsJsonIndented { get; set; } - /// - /// Gets or sets the body (as bytearray). - /// - public byte[]? BodyAsBytes { get; set; } + /// + /// Gets or sets the body (as bytearray). + /// + public byte[]? BodyAsBytes { get; set; } - /// - /// Gets or sets the body as a file. - /// - public string? BodyAsFile { get; set; } + /// + /// Gets or sets the body as a file. + /// + public string? BodyAsFile { get; set; } - /// - /// Is the body as file cached? - /// - public bool? BodyAsFileIsCached { get; set; } + /// + /// Is the body as file cached? + /// + public bool? BodyAsFileIsCached { get; set; } - /// - /// Gets or sets the body encoding. - /// - public EncodingModel? BodyEncoding { get; set; } + /// + /// Gets or sets the body encoding. + /// + public EncodingModel? BodyEncoding { get; set; } - /// - /// Use ResponseMessage Transformer. - /// - public bool? UseTransformer { get; set; } + /// + /// Use ResponseMessage Transformer. + /// + public bool? UseTransformer { get; set; } - /// - /// Gets the type of the transformer. - /// - public string? TransformerType { get; set; } + /// + /// Gets the type of the transformer. + /// + public string? TransformerType { get; set; } - /// - /// Use the Handlebars transformer for the content from the referenced BodyAsFile. - /// - public bool? UseTransformerForBodyAsFile { get; set; } + /// + /// Use the Handlebars transformer for the content from the referenced BodyAsFile. + /// + public bool? UseTransformerForBodyAsFile { get; set; } - /// - /// The ReplaceNodeOptions to use when transforming a JSON node. - /// - public string? TransformerReplaceNodeOptions { get; set; } + /// + /// The ReplaceNodeOptions to use when transforming a JSON node. + /// + public string? TransformerReplaceNodeOptions { get; set; } - /// - /// Gets or sets the headers. - /// - public IDictionary? Headers { get; set; } + /// + /// Gets or sets the headers. + /// + public IDictionary? Headers { get; set; } - /// - /// Gets or sets the Headers (Raw). - /// - public string? HeadersRaw { get; set; } + /// + /// Gets or sets the Headers (Raw). + /// + public string? HeadersRaw { get; set; } - /// - /// Gets or sets the delay in milliseconds. - /// - public int? Delay { get; set; } + /// + /// Gets or sets the delay in milliseconds. + /// + public int? Delay { get; set; } - /// - /// Gets or sets the minimum random delay in milliseconds. - /// - public int? MinimumRandomDelay { get; set; } + /// + /// Gets or sets the minimum random delay in milliseconds. + /// + public int? MinimumRandomDelay { get; set; } - /// - /// Gets or sets the maximum random delay in milliseconds. - /// - public int? MaximumRandomDelay { get; set; } + /// + /// Gets or sets the maximum random delay in milliseconds. + /// + public int? MaximumRandomDelay { get; set; } - /// - /// Gets or sets the Proxy URL. - /// - public string? ProxyUrl { get; set; } + /// + /// Gets or sets the Proxy URL. + /// + public string? ProxyUrl { get; set; } - /// - /// The client X509Certificate2 Thumbprint or SubjectName to use. - /// - public string? X509Certificate2ThumbprintOrSubjectName { get; set; } + /// + /// The client X509Certificate2 Thumbprint or SubjectName to use. + /// + public string? X509Certificate2ThumbprintOrSubjectName { get; set; } - /// - /// Gets or sets the fault. - /// - public FaultModel? Fault { get; set; } + /// + /// Gets or sets the fault. + /// + public FaultModel? Fault { get; set; } - /// - /// Gets or sets the WebProxy settings. - /// - public WebProxyModel? WebProxy { get; set; } - } + /// + /// Gets or sets the WebProxy settings. + /// + public WebProxyModel? WebProxy { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/IResponseMessage.cs b/src/WireMock.Net.Abstractions/IResponseMessage.cs index b5faa4cc..b1499b1a 100644 --- a/src/WireMock.Net.Abstractions/IResponseMessage.cs +++ b/src/WireMock.Net.Abstractions/IResponseMessage.cs @@ -44,7 +44,7 @@ public interface IResponseMessage /// Gets or sets the status code. /// object? StatusCode { get; } - + /// /// Adds the header. /// diff --git a/src/WireMock.Net/IMapping.cs b/src/WireMock.Net/IMapping.cs index c657b6bc..86f35dd5 100644 --- a/src/WireMock.Net/IMapping.cs +++ b/src/WireMock.Net/IMapping.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using WireMock.Matchers.Request; using WireMock.Models; @@ -120,7 +121,16 @@ public interface IMapping /// /// Use Fire and Forget for the defined webhook(s). [Optional] /// - public bool? UseWebhooksFireAndForget { get; set; } + bool? UseWebhooksFireAndForget { get; set; } + + /// + /// Data Object which can be used when WithTransformer is used. + /// e.g. lookup an path in this object using + /// + /// lookup data "1" + /// + /// + object? Data { get; set; } /// /// ProvideResponseAsync diff --git a/src/WireMock.Net/Json/DynamicJsonClassOptions.cs b/src/WireMock.Net/Json/DynamicJsonClassOptions.cs new file mode 100644 index 00000000..6d090b3d --- /dev/null +++ b/src/WireMock.Net/Json/DynamicJsonClassOptions.cs @@ -0,0 +1,10 @@ +// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq + +namespace WireMock.Json; + +internal class DynamicJsonClassOptions +{ + public IntegerBehavior IntegerConvertBehavior { get; set; } = IntegerBehavior.UseLong; + + public FloatBehavior FloatConvertBehavior { get; set; } = FloatBehavior.UseDouble; +} \ No newline at end of file diff --git a/src/WireMock.Net/Json/DynamicPropertyWithValue.cs b/src/WireMock.Net/Json/DynamicPropertyWithValue.cs new file mode 100644 index 00000000..55219734 --- /dev/null +++ b/src/WireMock.Net/Json/DynamicPropertyWithValue.cs @@ -0,0 +1,15 @@ +// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq + +using System.Linq.Dynamic.Core; + +namespace WireMock.Json; + +public class DynamicPropertyWithValue : DynamicProperty +{ + public object? Value { get; } + + public DynamicPropertyWithValue(string name, object? value) : base(name, value?.GetType() ?? typeof(object)) + { + Value = value; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Json/FloatBehavior.cs b/src/WireMock.Net/Json/FloatBehavior.cs new file mode 100644 index 00000000..0bfa8100 --- /dev/null +++ b/src/WireMock.Net/Json/FloatBehavior.cs @@ -0,0 +1,24 @@ +// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq + +namespace WireMock.Json; + +/// +/// Enum to define how to convert an Float in the Json Object. +/// +internal enum FloatBehavior +{ + /// + /// Convert all Float types in the Json Object to a double. (default) + /// + UseDouble = 0, + + /// + /// Convert all Float types in the Json Object to a float (unless overflow). + /// + UseFloat = 1, + + /// + /// Convert all Float types in the Json Object to a decimal (unless overflow). + /// + UseDecimal = 2 +} \ No newline at end of file diff --git a/src/WireMock.Net/Json/IntegerBehavior.cs b/src/WireMock.Net/Json/IntegerBehavior.cs new file mode 100644 index 00000000..6258abfc --- /dev/null +++ b/src/WireMock.Net/Json/IntegerBehavior.cs @@ -0,0 +1,20 @@ +// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq + +namespace WireMock.Json; + +/// +/// Enum to define how to convert an Integer in the Json Object. +/// +internal enum IntegerBehavior +{ + /// + /// Convert all Integer types in the Json Object to a int (unless overflow). + /// (default) + /// + UseInt = 0, + + /// + /// Convert all Integer types in the Json Object to a long. + /// + UseLong = 1 +} \ No newline at end of file diff --git a/src/WireMock.Net/Json/JObjectExtensions.cs b/src/WireMock.Net/Json/JObjectExtensions.cs new file mode 100644 index 00000000..1473a489 --- /dev/null +++ b/src/WireMock.Net/Json/JObjectExtensions.cs @@ -0,0 +1,202 @@ +// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Reflection; +using Newtonsoft.Json.Linq; + +namespace WireMock.Json; + +internal static class JObjectExtensions +{ + private class JTokenResolvers : Dictionary> + { + } + + private static readonly JTokenResolvers Resolvers = new() + { + { JTokenType.Array, ConvertJTokenArray }, + { JTokenType.Boolean, (jToken, _) => jToken.Value() }, + { JTokenType.Bytes, (jToken, _) => jToken.Value() }, + { JTokenType.Date, (jToken, _) => jToken.Value() }, + { JTokenType.Float, ConvertJTokenFloat }, + { JTokenType.Guid, (jToken, _) => jToken.Value() }, + { JTokenType.Integer, ConvertJTokenInteger }, + { JTokenType.None, (_, _) => null }, + { JTokenType.Null, (_, _) => null }, + { JTokenType.Object, ConvertJObject }, + { JTokenType.Property, ConvertJTokenProperty }, + { JTokenType.String, (jToken, _) => jToken.Value() }, + { JTokenType.TimeSpan, (jToken, _) => jToken.Value() }, + { JTokenType.Undefined, (_, _) => null }, + { JTokenType.Uri, (o, _) => o.Value() }, + }; + + internal static DynamicClass? ToDynamicJsonClass(this JObject? src, DynamicJsonClassOptions? options = null) + { + if (src == null) + { + return null; + } + + var dynamicPropertyWithValues = new List(); + + foreach (var prop in src.Properties()) + { + var value = Resolvers[prop.Type](prop.Value, options); + if (value != null) + { + dynamicPropertyWithValues.Add(new DynamicPropertyWithValue(prop.Name, value)); + } + } + + return CreateInstance(dynamicPropertyWithValues); + } + + internal static IEnumerable ToDynamicClassArray(this JArray? src, DynamicJsonClassOptions? options = null) + { + if (src == null) + { + return new object?[0]; + } + + return ConvertJTokenArray(src, options); + } + + private static object? ConvertJObject(JToken arg, DynamicJsonClassOptions? options = null) + { + if (arg is JObject asJObject) + { + return asJObject.ToDynamicJsonClass(options); + } + + return GetResolverFor(arg)(arg, options); + } + + private static object PassThrough(JToken arg, DynamicJsonClassOptions? options) + { + return arg; + } + + private static Func GetResolverFor(JToken arg) + { + return Resolvers.TryGetValue(arg.Type, out var result) ? result : PassThrough; + } + + private static object ConvertJTokenFloat(JToken arg, DynamicJsonClassOptions? options = null) + { + if (arg.Type != JTokenType.Float) + { + throw new InvalidOperationException($"Unable to convert {nameof(JToken)} of type: {arg.Type} to double or float."); + } + + if (options?.FloatConvertBehavior == FloatBehavior.UseFloat) + { + try + { + return arg.Value(); + } + catch + { + return arg.Value(); + } + } + + if (options?.FloatConvertBehavior == FloatBehavior.UseDecimal) + { + try + { + return arg.Value(); + } + catch + { + return arg.Value(); + } + } + + + return arg.Value(); + } + + private static object ConvertJTokenInteger(JToken arg, DynamicJsonClassOptions? options = null) + { + if (arg.Type != JTokenType.Integer) + { + throw new InvalidOperationException($"Unable to convert {nameof(JToken)} of type: {arg.Type} to long or int."); + } + + var longValue = arg.Value(); + + if (options is null || options.IntegerConvertBehavior == IntegerBehavior.UseInt) + { + if (longValue is >= int.MinValue and <= int.MaxValue) + { + return Convert.ToInt32(longValue); + } + } + + return longValue; + } + + private static object? ConvertJTokenProperty(JToken arg, DynamicJsonClassOptions? options = null) + { + var resolver = GetResolverFor(arg); + if (resolver is null) + { + throw new InvalidOperationException($"Unable to handle {nameof(JToken)} of type: {arg.Type}."); + } + + return resolver(arg, options); + } + + private static IEnumerable ConvertJTokenArray(JToken arg, DynamicJsonClassOptions? options = null) + { + if (arg is not JArray array) + { + throw new InvalidOperationException($"Unable to convert {nameof(JToken)} of type: {arg.Type} to {nameof(JArray)}."); + } + + var result = new List(); + foreach (var item in array) + { + result.Add(ConvertJObject(item)); + } + + var distinctType = FindSameTypeOf(result); + return distinctType == null ? result.ToArray() : ConvertToTypedArray(result, distinctType); + } + + private static Type? FindSameTypeOf(IEnumerable src) + { + var types = src.Select(o => o?.GetType()).Distinct().OfType().ToArray(); + return types.Length == 1 ? types[0] : null; + } + + private static IEnumerable ConvertToTypedArray(IEnumerable src, Type newType) + { + var method = ConvertToTypedArrayGenericMethod.MakeGenericMethod(newType); + return (IEnumerable)method.Invoke(null, new object[] { src })!; + } + + private static readonly MethodInfo ConvertToTypedArrayGenericMethod = typeof(JObjectExtensions).GetMethod(nameof(ConvertToTypedArrayGeneric), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static T[] ConvertToTypedArrayGeneric(IEnumerable src) + { + return src.Cast().ToArray(); + } + + public static DynamicClass CreateInstance(IList dynamicPropertiesWithValue, bool createParameterCtor = true) + { + var type = DynamicClassFactory.CreateType(dynamicPropertiesWithValue.Cast().ToArray(), createParameterCtor); + var dynamicClass = (DynamicClass)Activator.CreateInstance(type); + foreach (var dynamicPropertyWithValue in dynamicPropertiesWithValue.Where(p => p.Value != null)) + { + dynamicClass.SetDynamicPropertyValue(dynamicPropertyWithValue.Name, dynamicPropertyWithValue.Value!); + } + + return dynamicClass; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index 8b875df7..e5823a1e 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -72,6 +72,9 @@ public class Mapping : IMapping /// public ITimeSettings? TimeSettings { get; } + /// + public object? Data { get; set; } + /// /// Initializes a new instance of the class. /// @@ -91,6 +94,7 @@ public class Mapping : IMapping /// The Webhooks. [Optional] /// Use Fire and Forget for the defined webhook(s). [Optional] /// The TimeSettings. [Optional] + /// The data object. [Optional] public Mapping( Guid guid, DateTime updatedAt, @@ -107,7 +111,8 @@ public class Mapping : IMapping int? stateTimes, IWebhook[]? webhooks, bool? useWebhooksFireAndForget, - ITimeSettings? timeSettings) + ITimeSettings? timeSettings, + object? data) { Guid = guid; UpdatedAt = updatedAt; @@ -125,6 +130,7 @@ public class Mapping : IMapping Webhooks = webhooks; UseWebhooksFireAndForget = useWebhooksFireAndForget; TimeSettings = timeSettings; + Data = data; } /// diff --git a/src/WireMock.Net/Matchers/LinqMatcher.cs b/src/WireMock.Net/Matchers/LinqMatcher.cs index 80167b8c..f8069a8d 100644 --- a/src/WireMock.Net/Matchers/LinqMatcher.cs +++ b/src/WireMock.Net/Matchers/LinqMatcher.cs @@ -5,8 +5,8 @@ using AnyOfTypes; using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Extensions; +using WireMock.Json; using WireMock.Models; -using WireMock.Util; namespace WireMock.Matchers; @@ -100,38 +100,55 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher { double match = MatchScores.Mismatch; - JObject value; - switch (input) + JArray jArray; + try { - case JObject valueAsJObject: - value = valueAsJObject; - break; - - case { } valueAsObject: - value = JObject.FromObject(valueAsObject); - break; - - default: - return MatchScores.Mismatch; + jArray = new JArray { input }; + } + catch + { + jArray = new JArray { JToken.FromObject(input) }; } + //enumerable = jArray.ToDynamicClassArray(); + + //JObject value; + //switch (input) + //{ + // case JObject valueAsJObject: + // value = valueAsJObject; + // break; + + // case { } valueAsObject: + // value = JObject.FromObject(valueAsObject); + // break; + + // default: + // return MatchScores.Mismatch; + //} + // Convert a single object to a Queryable JObject-list with 1 entry. - var queryable1 = new[] { value }.AsQueryable(); + //var queryable1 = new[] { value }.AsQueryable(); + var queryable = jArray.ToDynamicClassArray().AsQueryable(); try { // Generate the DynamicLinq select statement. - string dynamicSelect = JsonUtils.GenerateDynamicLinqStatement(value); + //string dynamicSelect = JsonUtils.GenerateDynamicLinqStatement(value); // Execute DynamicLinq Select statement. - var queryable2 = queryable1.Select(dynamicSelect); + //var queryable2 = queryable1.Select(dynamicSelect); // Use the Any(...) method to check if the result matches. - match = MatchScores.ToScore(_patterns.Select(pattern => queryable2.Any(pattern)).ToArray(), MatchOperator); + + var patternsAsStringArray = _patterns.Select(p => p.GetPattern()).ToArray(); + var scores = patternsAsStringArray.Select(p => queryable.Any(p)).ToArray(); + + match = MatchScores.ToScore(_patterns.Select(pattern => queryable.Any(pattern.GetPattern())).ToArray(), MatchOperator); return MatchBehaviourHelper.Convert(MatchBehaviour, match); } - catch + catch (Exception e) { if (ThrowException) { diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 13c482b4..00e2de48 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -107,14 +107,12 @@ public partial class Response : IResponseBuilder /// /// Initializes a new instance of the class. /// - /// - /// The response. - /// + /// The response. private Response(ResponseMessage responseMessage) { ResponseMessage = responseMessage; } - + /// [PublicAPI] public IResponseBuilder WithStatusCode(int code) diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 4bc1b4a8..bf287daa 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -181,6 +181,7 @@ internal class MappingConverter Scenario = mapping.Scenario, WhenStateIs = mapping.ExecutionConditionState, SetStateTo = mapping.NextState, + Data = mapping.Data, Request = new RequestModel { Headers = headerMatchers.Any() ? headerMatchers.Select(hm => new HeaderModel diff --git a/src/WireMock.Net/Serialization/ProxyMappingConverter.cs b/src/WireMock.Net/Serialization/ProxyMappingConverter.cs index 157e61a7..1531a0ce 100644 --- a/src/WireMock.Net/Serialization/ProxyMappingConverter.cs +++ b/src/WireMock.Net/Serialization/ProxyMappingConverter.cs @@ -177,7 +177,8 @@ internal class ProxyMappingConverter stateTimes: null, webhooks: null, useWebhooksFireAndForget: null, - timeSettings: null + timeSettings: null, + data: mapping?.Data ); } } \ No newline at end of file diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs index 55751738..2f81df7e 100644 --- a/src/WireMock.Net/Server/IRespondWithAProvider.cs +++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs @@ -166,4 +166,14 @@ public interface IRespondWithAProvider bool useTransformer = true, TransformerType transformerType = TransformerType.Handlebars ); + + /// + /// Data Object which can be used when WithTransformer is used. + /// e.g. lookup an path in this object using + /// The data dictionary object. + /// + /// lookup data "1" + /// + /// + IRespondWithAProvider WithData(object data); } \ No newline at end of file diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index ed9436b9..dc658183 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -2,6 +2,7 @@ // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. using System; using System.Collections.Generic; +using JetBrains.Annotations; using Stef.Validation; using WireMock.Matchers.Request; using WireMock.Models; @@ -39,6 +40,8 @@ internal class RespondWithAProvider : IRespondWithAProvider public ITimeSettings? TimeSettings { get; private set; } + public object? Data { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -88,10 +91,20 @@ internal class RespondWithAProvider : IRespondWithAProvider _timesInSameState, Webhooks, _useWebhookFireAndForget, - TimeSettings); + TimeSettings, + Data); + _registrationCallback(mapping, _saveToFile); } + /// + [PublicAPI] + public IRespondWithAProvider WithData(object data) + { + Data = data; + return this; + } + /// public IRespondWithAProvider WithGuid(string guid) { diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 2bcd0ff9..052440ad 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -46,7 +46,7 @@ public partial class WireMockServer } var respondProvider = Given(requestBuilder, mappingModel.SaveToFile == true); - + if (guid != null) { respondProvider = respondProvider.WithGuid(guid.Value); @@ -56,6 +56,11 @@ public partial class WireMockServer respondProvider = respondProvider.WithGuid(mappingModel.Guid.Value); } + if (mappingModel.Data != null) + { + respondProvider = respondProvider.WithData(mappingModel.Data); + } + var timeSettings = TimeSettingsMapper.Map(mappingModel.TimeSettings); if (timeSettings != null) { diff --git a/src/WireMock.Net/Transformers/TransformModel.cs b/src/WireMock.Net/Transformers/TransformModel.cs index b089baeb..d2bdbdd7 100644 --- a/src/WireMock.Net/Transformers/TransformModel.cs +++ b/src/WireMock.Net/Transformers/TransformModel.cs @@ -10,4 +10,6 @@ internal struct TransformModel public IRequestMessage request { get; set; } public IResponseMessage? response { get; set; } + + public object data { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Transformer.cs b/src/WireMock.Net/Transformers/Transformer.cs index 4c47ad1b..74cd6992 100644 --- a/src/WireMock.Net/Transformers/Transformer.cs +++ b/src/WireMock.Net/Transformers/Transformer.cs @@ -5,6 +5,7 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Stef.Validation; +using WireMock.ResponseBuilders; using WireMock.Settings; using WireMock.Types; using WireMock.Util; @@ -115,7 +116,8 @@ internal class Transformer : ITransformer { mapping = mapping, request = request, - response = response + response = response, + data = mapping.Data ?? new { } }); } diff --git a/src/WireMock.Net/Util/JsonUtils.cs b/src/WireMock.Net/Util/JsonUtils.cs index 59700d2c..3d69ba01 100644 --- a/src/WireMock.Net/Util/JsonUtils.cs +++ b/src/WireMock.Net/Util/JsonUtils.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -108,103 +106,4 @@ internal static class JsonUtils _ => throw new NotSupportedException($"Unable to convert value to {typeof(T)}.") }; } - - /// - /// Based on / copied from Handlebars.Net Handlebars.Net.Helpers.Utils.JsonUtils - /// - public static string GenerateDynamicLinqStatement(JToken jsonObject) - { - var lines = new List(); - WalkNode(jsonObject, null, null, lines); - - return lines.First(); - } - - private static void WalkNode(JToken node, string? path, string? propertyName, List lines) - { - switch (node.Type) - { - case JTokenType.Object: - ProcessObject(node, propertyName, lines); - break; - - case JTokenType.Array: - ProcessArray(node, propertyName, lines); - break; - - default: - ProcessItem(node, path ?? "it", propertyName, lines); - break; - } - } - - private static void ProcessObject(JToken node, string? propertyName, List lines) - { - var items = new List(); - var text = new StringBuilder("new ("); - - // In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions. - foreach (var child in node.Children().ToArray()) - { - WalkNode(child.Value, child.Path, child.Name, items); - } - - text.Append(string.Join(", ", items)); - text.Append(")"); - - if (!string.IsNullOrEmpty(propertyName)) - { - text.AppendFormat(" as {0}", propertyName); - } - - lines.Add(text.ToString()); - } - - private static void ProcessArray(JToken node, string? propertyName, List lines) - { - var items = new List(); - var text = new StringBuilder("(new [] { "); - - // In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions. - var idx = 0; - foreach (var child in node.Children().ToArray()) - { - WalkNode(child, $"{node.Path}[{idx}]", null, items); - idx++; - } - - text.Append(string.Join(", ", items)); - text.Append("})"); - - if (!string.IsNullOrEmpty(propertyName)) - { - text.AppendFormat(" as {0}", propertyName); - } - - lines.Add(text.ToString()); - } - - private static void ProcessItem(JToken node, string path, string? propertyName, List lines) - { - var castText = node.Type switch - { - JTokenType.Boolean => $"bool({path})", - JTokenType.Date => $"DateTime({path})", - JTokenType.Float => $"double({path})", - JTokenType.Guid => $"Guid({path})", - JTokenType.Integer => $"long({path})", - JTokenType.Null => "null", - JTokenType.String => $"string({path})", - JTokenType.TimeSpan => $"TimeSpan({path})", - JTokenType.Uri => $"Uri({path})", - _ => throw new NotSupportedException($"JTokenType '{node.Type}' cannot be converted to a Dynamic Linq cast operator.") - }; - - if (!string.IsNullOrEmpty(propertyName)) - { - castText += $" as {propertyName}"; - } - - lines.Add(castText); - } } \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 5154eda5..1c3289a3 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -53,13 +53,13 @@ - + - + @@ -177,13 +177,13 @@ - - - - - - - + + + + + + + diff --git a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs index 1d13da61..4e253fc8 100644 --- a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs @@ -177,7 +177,7 @@ public class WireMockMiddlewareTests _mappingMock.SetupGet(m => m.Provider).Returns(responseBuilder); _mappingMock.SetupGet(m => m.Settings).Returns(settings); - var newMappingFromProxy = new Mapping(Guid.NewGuid(), _updatedAt, string.Empty, string.Empty, null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null); + var newMappingFromProxy = new Mapping(Guid.NewGuid(), _updatedAt, string.Empty, string.Empty, null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null, null); _mappingMock.Setup(m => m.ProvideResponseAsync(It.IsAny())).ReturnsAsync((new ResponseMessage(), newMappingFromProxy)); var requestBuilder = Request.Create().UsingAnyMethod(); @@ -231,7 +231,7 @@ public class WireMockMiddlewareTests _mappingMock.SetupGet(m => m.Provider).Returns(responseBuilder); _mappingMock.SetupGet(m => m.Settings).Returns(settings); - var newMappingFromProxy = new Mapping(Guid.NewGuid(), _updatedAt, "my-title", "my-description", null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null); + var newMappingFromProxy = new Mapping(Guid.NewGuid(), _updatedAt, "my-title", "my-description", null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null, data: null); _mappingMock.Setup(m => m.ProvideResponseAsync(It.IsAny())).ReturnsAsync((new ResponseMessage(), newMappingFromProxy)); var requestBuilder = Request.Create().UsingAnyMethod(); diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsLinqTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsLinqTests.cs index 51d794df..c9c00335 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsLinqTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsLinqTests.cs @@ -163,22 +163,6 @@ public class ResponseWithHandlebarsLinqTests Check.ThatAsyncCode(() => responseBuilder.ProvideResponseAsync(mappingMock.Object, request, _settings)).Throws(); } - [Fact] - public void Response_ProvideResponse_Handlebars_Linq1_Throws_ArgumentNullException() - { - // Assign - var body = new BodyData(); - - var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "POST", "::1", body); - - var responseBuilder = Response.Create() - .WithBodyAsJson(new { x = "{{Linq request.body 'Name'}}" }) - .WithTransformer(); - - // Act - Check.ThatAsyncCode(() => responseBuilder.ProvideResponseAsync(mappingMock.Object, request, _settings)).Throws(); - } - [Fact] public void Response_ProvideResponse_Handlebars_Linq1_Throws_HandlebarsException() { diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs index 604aad17..f8ba854b 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs @@ -782,4 +782,31 @@ public class ResponseWithTransformerTests response.Message.BodyData!.BodyAsString.Should().Be(text); response.Message.BodyData.Encoding.Should().Be(enc); } + + [Theory] + [InlineData("/wiremock-data/1", "one")] + [InlineData("/wiremock-data/2", "two")] + [InlineData("/wiremock-data/3", "N/A")] + public async Task Response_ProvideResponse_Handlebars_DataObject(string path, string expected) + { + // Arrange + var request = new RequestMessage(new UrlDetails("https://localhost" + path), "POST", ClientIp); + var data = new Dictionary + { + { "1", "one" }, + { "2", "two" } + }; + + var responseBuilder = Response.Create() + .WithBody("{{lookup data request.PathSegments.[1] 'N/A'}}") + .WithTransformer(); + + _mappingMock.SetupGet(m => m.Data).Returns(data); + + // Act + var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); + + // Assert + response.Message.BodyData!.BodyAsString.Should().Be(expected); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs index 361c56b7..1e629313 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs @@ -119,7 +119,25 @@ public partial class MappingConverterTests .WithDelay(12345) .WithTransformer(); - return new Mapping(guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null); + return new Mapping( + guid, + _updatedAt, + string.Empty, + string.Empty, + null, + _settings, + request, + response, + 42, + null, + null, + null, + null, + null, + false, + null, + data: null + ); } } #endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs index 4c1561db..ec968b51 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs @@ -57,7 +57,7 @@ public partial class MappingConverterTests } } }; - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, false, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -130,7 +130,7 @@ public partial class MappingConverterTests } } }; - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, true, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, true, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -168,7 +168,7 @@ public partial class MappingConverterTests var description = "my-description"; var request = Request.Create(); var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, title, description, null, _settings, request, response, 0, null, null, null, null, null, false, null); + var mapping = new Mapping(_guid, _updatedAt, title, description, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -188,7 +188,7 @@ public partial class MappingConverterTests // Assign var request = Request.Create(); var response = Response.Create().WithBodyAsJson(new { x = "x" }).WithTransformer(); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -217,7 +217,7 @@ public partial class MappingConverterTests End = end, TTL = ttl }; - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, timeSettings); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, timeSettings, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -248,7 +248,7 @@ public partial class MappingConverterTests { var request = Request.Create(); var response = Response.Create().WithDelay(test.Delay); - var mapping = new Mapping(Guid.NewGuid(), _updatedAt, string.Empty, string.Empty, string.Empty, _settings, request, response, 42, null, null, null, null, null, false, null); + var mapping = new Mapping(Guid.NewGuid(), _updatedAt, string.Empty, string.Empty, string.Empty, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -266,7 +266,7 @@ public partial class MappingConverterTests var delay = 1000; var request = Request.Create(); var response = Response.Create().WithDelay(delay); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -286,7 +286,7 @@ public partial class MappingConverterTests int minimumDelay = 1000; var request = Request.Create(); var response = Response.Create().WithRandomDelay(minimumDelay); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -309,7 +309,7 @@ public partial class MappingConverterTests int maximumDelay = 2000; var request = Request.Create(); var response = Response.Create().WithRandomDelay(minimumDelay, maximumDelay); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index b8fc6813..2892bcdd 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -12,7 +12,7 @@ true ../../src/WireMock.Net/WireMock.Net.snk - + true @@ -63,12 +63,13 @@ + + - @@ -97,7 +98,8 @@ - + + @@ -117,8 +119,8 @@ PreserveNewest - PreserveNewest - WireMockServerTests.ClientCertificate.cs + PreserveNewest + WireMockServerTests.ClientCertificate.cs @@ -127,7 +129,7 @@ - + \ No newline at end of file