* Fix construction of path in OpenApiParser (#1265)

* Server-Sent Events (#1269)

* Server Side Events

* fixes

* await HandleSseStringAsync(responseMessage, response, bodyData);

* 1.7.5-preview-01

* IBlockingQueue

* 1.7.5-preview-02 (03 April 2025)

* IBlockingQueue

* ...

* Support OpenApi V31 (#1279)

* Support OpenApi V31

* Update src/WireMock.Net.OpenApiParser/Extensions/OpenApiSchemaExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fx

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add ProtoDefinitionHelper.FromDirectory (#1263)

* Add ProtoDefinitionHelper.FromDirectory

* .

* unix-windows

* move test

* imports in the proto files indeed should use a forward slash

* updates

* .

* private Func<IdOrTexts> ProtoDefinitionFunc()

* OpenTelemetry

* .

* fix path utils

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Stef Heyenrath
2025-04-23 11:51:44 +02:00
committed by GitHub
parent fc0f82db33
commit 4368e3cde6
66 changed files with 3275 additions and 1002 deletions

View File

@@ -0,0 +1,30 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
namespace WireMock.Models;
/// <summary>
/// A simple implementation for a Blocking Queue.
/// </summary>
/// <typeparam name="T">Specifies the type of elements in the queue.</typeparam>
public interface IBlockingQueue<T>
{
/// <summary>
/// Writes an item to the queue and signals that an item is available.
/// </summary>
/// <param name="item">The item to be added to the queue.</param>
void Write(T item);
/// <summary>
/// Tries to read an item from the queue. Waits until an item is available or the timeout occurs.
/// </summary>
/// <param name="item">The item read from the queue, or default if the timeout occurs.</param>
/// <returns>True if an item was successfully read; otherwise, false.</returns>
bool TryRead([NotNullWhen(true)] out T? item);
/// <summary>
/// Closes the queue and signals all waiting threads.
/// </summary>
public void Close();
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using WireMock.Models;
using WireMock.Types;
@@ -71,7 +72,7 @@ public interface IBodyData
Encoding? Encoding { get; set; }
/// <summary>
/// Defines if this BodyData is the result of a dynamically created response-string. (
/// Defines if this BodyData is the result of a dynamically created response-string.
/// </summary>
public string? IsFuncUsed { get; set; }
@@ -86,4 +87,14 @@ public interface IBodyData
/// </summary>
public string? ProtoBufMessageType { get; set; }
#endregion
/// <summary>
/// Defines the queue to use for Server-Sent Events (string).
/// </summary>
public IBlockingQueue<string?>? SseStringQueue { get; set; }
/// <summary>
/// Defines if the body is using Server-Sent Events (string).
/// </summary>
public Task? BodyAsSseStringTask { get; set; }
}

View File

@@ -45,5 +45,10 @@ public enum BodyType
/// <summary>
/// Body is a ProtoBuf Byte array
/// </summary>
ProtoBuf
ProtoBuf,
/// <summary>
/// Use Server-Sent Events (string)
/// </summary>
SseString
}

View File

@@ -61,4 +61,11 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
// Copyright © WireMock.Net
#if NET46 || NETSTANDARD2_0
#if NET46 || NET47 || NETSTANDARD2_0
using System.Collections.Generic;
namespace WireMock.Net.OpenApiParser.Extensions;

View File

@@ -1,60 +1,53 @@
// Copyright © WireMock.Net
using System.Linq;
using System.Text.Json;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using WireMock.Net.OpenApiParser.Types;
namespace WireMock.Net.OpenApiParser.Extensions;
internal static class OpenApiSchemaExtensions
{
/// <summary>
/// https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger
/// </summary>
public static bool TryGetXNullable(this OpenApiSchema schema, out bool value)
public static bool TryGetXNullable(this IOpenApiSchema schema, out bool value)
{
value = false;
if (schema.Extensions.TryGetValue("x-nullable", out var e) && e is OpenApiBoolean openApiBoolean)
if (schema.Extensions != null && schema.Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny { Node: { } jsonNode })
{
value = openApiBoolean.Value;
value = jsonNode.GetValueKind() == JsonValueKind.True;
return true;
}
return false;
}
public static SchemaType GetSchemaType(this OpenApiSchema? schema)
public static JsonSchemaType? GetSchemaType(this IOpenApiSchema? schema, out bool isNullable)
{
isNullable = false;
if (schema == null)
{
return SchemaType.Unknown;
return null;
}
if (schema.Type == null)
{
if (schema.AllOf.Any() || schema.AnyOf.Any())
if (schema.AllOf?.Any() == true || schema.AnyOf?.Any() == true)
{
return SchemaType.Object;
return JsonSchemaType.Object;
}
}
return schema.Type switch
{
"object" => SchemaType.Object,
"array" => SchemaType.Array,
"integer" => SchemaType.Integer,
"number" => SchemaType.Number,
"boolean" => SchemaType.Boolean,
"string" => SchemaType.String,
"file" => SchemaType.File,
_ => SchemaType.Unknown
};
isNullable = (schema.Type | JsonSchemaType.Null) == JsonSchemaType.Null || (schema.TryGetXNullable(out var xNullable) && xNullable);
// Removes the Null flag from the schema.Type, ensuring the returned value represents a non-nullable type.
return schema.Type & ~JsonSchemaType.Null;
}
public static SchemaFormat GetSchemaFormat(this OpenApiSchema? schema)
public static SchemaFormat GetSchemaFormat(this IOpenApiSchema? schema)
{
switch (schema?.Format)
{

View File

@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Microsoft.OpenApi.Reader;
using Stef.Validation;
using WireMock.Net.OpenApiParser.Settings;
using WireMock.Server;
@@ -17,7 +17,7 @@ namespace WireMock.Net.OpenApiParser.Extensions;
public static class WireMockServerExtensions
{
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 file.
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
@@ -29,7 +29,7 @@ public static class WireMockServerExtensions
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 file.
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
@@ -47,7 +47,7 @@ public static class WireMockServerExtensions
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 stream.
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
@@ -59,7 +59,7 @@ public static class WireMockServerExtensions
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 stream.
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
@@ -78,7 +78,7 @@ public static class WireMockServerExtensions
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 document.
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 document.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="document">The OpenAPI document to use as mappings.</param>

View File

@@ -3,7 +3,7 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Microsoft.OpenApi.Reader;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Settings;
@@ -17,7 +17,7 @@ public interface IWireMockOpenApiParser
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file.</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic);
@@ -25,7 +25,7 @@ public interface IWireMockOpenApiParser
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>

View File

@@ -3,20 +3,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;
using Microsoft.OpenApi.Models.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Stef.Validation;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Extensions;
using WireMock.Net.OpenApiParser.Settings;
using WireMock.Net.OpenApiParser.Types;
using WireMock.Net.OpenApiParser.Utils;
using SystemTextJsonSerializer = System.Text.Json.JsonSerializer;
namespace WireMock.Net.OpenApiParser.Mappers;
@@ -39,56 +38,40 @@ internal class OpenApiPathsMapper
.OrderBy(p => p.Key)
.Select(p => MapPath(p.Key, p.Value, servers))
.SelectMany(x => x)
.ToArray() ??
Array.Empty<MappingModel>();
.ToArray() ?? [];
}
private IReadOnlyList<MappingModel> MapPaths(OpenApiPaths? paths, IList<OpenApiServer> servers)
private IReadOnlyList<MappingModel> MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
{
return paths?
.OrderBy(p => p.Key)
.Select(p => MapPath(p.Key, p.Value, servers))
.SelectMany(x => x)
.ToArray() ??
Array.Empty<MappingModel>();
}
private IReadOnlyList<MappingModel> MapPath(string path, OpenApiPathItem pathItem, IList<OpenApiServer> servers)
{
return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray();
return pathItem.Operations?.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray() ?? [];
}
private MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation, IList<OpenApiServer> servers)
{
var queryParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Query);
var pathParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Path);
var headers = operation.Parameters.Where(p => p.In == ParameterLocation.Header);
var queryParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Query) ?? [];
var pathParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Path) ?? [];
var headers = operation.Parameters?.Where(p => p.In == ParameterLocation.Header) ?? [];
var response = operation.Responses.FirstOrDefault();
var response = operation?.Responses?.FirstOrDefault() ?? new KeyValuePair<string, IOpenApiResponse>();
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out string? responseContentType);
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out var responseContentType);
var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema;
var responseExample = responseContent?.Example;
var responseSchemaExample = responseContent?.Schema?.Example;
var body = responseExample != null ? MapOpenApiAnyToJToken(responseExample) :
responseSchemaExample != null ? MapOpenApiAnyToJToken(responseSchemaExample) :
MapSchemaToObject(responseSchema);
var responseBody = responseExample ?? responseSchemaExample ?? MapSchemaToObject(responseSchema);
var requestBodyModel = new BodyModel();
if (operation.RequestBody != null && operation.RequestBody.Content != null && operation.RequestBody.Required)
{
var request = operation.RequestBody.Content;
TryGetContent(request, out OpenApiMediaType? requestContent, out _);
TryGetContent(request, out var requestContent, out _);
var requestBodySchema = operation.RequestBody.Content.First().Value?.Schema;
var requestBodyExample = requestContent!.Example;
var requestBodySchemaExample = requestContent.Schema?.Example;
var requestBodyMapped = requestBodyExample != null ? MapOpenApiAnyToJToken(requestBodyExample) :
requestBodySchemaExample != null ? MapOpenApiAnyToJToken(requestBodySchemaExample) :
MapSchemaToObject(requestBodySchema);
var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema);
requestBodyModel = MapRequestBody(requestBodyMapped);
}
@@ -102,8 +85,8 @@ internal class OpenApiPathsMapper
Guid = Guid.NewGuid(),
Request = new RequestModel
{
Methods = new[] { httpMethod },
Path = MapBasePath(servers) + MapPathWithParameters(path, pathParameters),
Methods = [httpMethod],
Path = PathUtils.Combine(MapBasePath(servers), MapPathWithParameters(path, pathParameters)),
Params = MapQueryParameters(queryParameters),
Headers = MapRequestHeaders(headers),
Body = requestBodyModel
@@ -112,12 +95,12 @@ internal class OpenApiPathsMapper
{
StatusCode = httpStatusCode,
Headers = MapHeaders(responseContentType, response.Value?.Headers),
BodyAsJson = body
BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null
}
};
}
private BodyModel? MapRequestBody(object? requestBody)
private BodyModel? MapRequestBody(JsonNode? requestBody)
{
if (requestBody == null)
{
@@ -129,7 +112,7 @@ internal class OpenApiPathsMapper
Matcher = new MatcherModel
{
Name = "JsonMatcher",
Pattern = JsonConvert.SerializeObject(requestBody, Formatting.Indented),
Pattern = SystemTextJsonSerializer.Serialize(requestBody, new JsonSerializerOptions { WriteIndented = true }),
IgnoreCase = _settings.RequestBodyIgnoreCase
}
};
@@ -160,117 +143,103 @@ internal class OpenApiPathsMapper
return true;
}
private object? MapSchemaToObject(OpenApiSchema? schema, string? name = null)
private JsonNode? MapSchemaToObject(IOpenApiSchema? schema)
{
if (schema == null)
{
return null;
}
switch (schema.GetSchemaType())
switch (schema.GetSchemaType(out _))
{
case SchemaType.Array:
var jArray = new JArray();
for (int i = 0; i < _settings.NumberOfArrayItems; i++)
case JsonSchemaType.Array:
var array = new JsonArray();
for (var i = 0; i < _settings.NumberOfArrayItems; i++)
{
if (schema.Items.Properties.Count > 0)
if (schema.Items?.Properties?.Count > 0)
{
var arrayItem = new JObject();
var item = new JsonObject();
foreach (var property in schema.Items.Properties)
{
var objectValue = MapSchemaToObject(property.Value, property.Key);
if (objectValue is JProperty jp)
{
arrayItem.Add(jp);
}
else
{
arrayItem.Add(new JProperty(property.Key, objectValue));
}
item[property.Key] = MapSchemaToObject(property.Value);
}
jArray.Add(arrayItem);
array.Add(item);
}
else
{
var arrayItem = MapSchemaToObject(schema.Items, name: null); // Set name to null to force JObject instead of JProperty
jArray.Add(arrayItem);
var arrayItem = MapSchemaToObject(schema.Items);
array.Add(arrayItem);
}
}
if (schema.AllOf.Count > 0)
if (schema.AllOf?.Count > 0)
{
jArray.Add(MapSchemaAllOfToObject(schema));
array.Add(MapSchemaAllOfToObject(schema));
}
return jArray;
return array;
case SchemaType.Boolean:
case SchemaType.Integer:
case SchemaType.Number:
case SchemaType.String:
case JsonSchemaType.Boolean:
case JsonSchemaType.Integer:
case JsonSchemaType.Number:
case JsonSchemaType.String:
return _exampleValueGenerator.GetExampleValue(schema);
case SchemaType.Object:
var propertyAsJObject = new JObject();
foreach (var schemaProperty in schema.Properties)
case JsonSchemaType.Object:
var propertyAsJsonObject = new JsonObject();
foreach (var schemaProperty in schema.Properties ?? new Dictionary<string, IOpenApiSchema>())
{
propertyAsJObject.Add(MapPropertyAsJObject(schemaProperty.Value, schemaProperty.Key));
propertyAsJsonObject[schemaProperty.Key] = MapPropertyAsJsonNode(schemaProperty.Value);
}
if (schema.AllOf.Count > 0)
if (schema.AllOf?.Count > 0)
{
foreach (var group in schema.AllOf.SelectMany(p => p.Properties).GroupBy(x => x.Key))
foreach (var group in schema.AllOf.SelectMany(p => p.Properties ?? new Dictionary<string, IOpenApiSchema>()).GroupBy(x => x.Key))
{
propertyAsJObject.Add(MapPropertyAsJObject(group.First().Value, group.Key));
propertyAsJsonObject[group.Key] = MapPropertyAsJsonNode(group.First().Value);
}
}
return name != null ? new JProperty(name, propertyAsJObject) : propertyAsJObject;
return propertyAsJsonObject;
default:
return null;
}
}
private JObject MapSchemaAllOfToObject(OpenApiSchema schema)
private JsonObject MapSchemaAllOfToObject(IOpenApiSchema schema)
{
var arrayItem = new JObject();
foreach (var property in schema.AllOf)
var arrayItem = new JsonObject();
foreach (var property in schema.AllOf ?? [])
{
foreach (var item in property.Properties)
foreach (var item in property.Properties ?? new Dictionary<string, IOpenApiSchema>())
{
arrayItem.Add(MapPropertyAsJObject(item.Value, item.Key));
arrayItem[item.Key] = MapPropertyAsJsonNode(item.Value);
}
}
return arrayItem;
}
private object MapPropertyAsJObject(OpenApiSchema openApiSchema, string key)
private JsonNode? MapPropertyAsJsonNode(IOpenApiSchema openApiSchema)
{
if (openApiSchema.GetSchemaType() == SchemaType.Object || openApiSchema.GetSchemaType() == SchemaType.Array)
var schemaType = openApiSchema.GetSchemaType(out _);
if (schemaType is JsonSchemaType.Object or JsonSchemaType.Array)
{
var mapped = MapSchemaToObject(openApiSchema, key);
if (mapped is JProperty jp)
{
return jp;
}
return new JProperty(key, mapped);
return MapSchemaToObject(openApiSchema);
}
// bool propertyIsNullable = openApiSchema.Nullable || (openApiSchema.TryGetXNullable(out bool x) && x);
return new JProperty(key, _exampleValueGenerator.GetExampleValue(openApiSchema));
return _exampleValueGenerator.GetExampleValue(openApiSchema);
}
private string MapPathWithParameters(string path, IEnumerable<OpenApiParameter>? parameters)
private string MapPathWithParameters(string path, IEnumerable<IOpenApiParameter>? parameters)
{
if (parameters == null)
{
return path;
}
string newPath = path;
var newPath = path;
foreach (var parameter in parameters)
{
var exampleMatcherModel = GetExampleMatcherModel(parameter.Schema, _settings.PathPatternToUse);
@@ -280,93 +249,56 @@ internal class OpenApiPathsMapper
return newPath;
}
private string MapBasePath(IList<OpenApiServer>? servers)
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, IOpenApiHeader>? headers)
{
if (servers == null || servers.Count == 0)
{
return string.Empty;
}
OpenApiServer server = servers.First();
if (Uri.TryCreate(server.Url, UriKind.RelativeOrAbsolute, out Uri uriResult))
{
return uriResult.IsAbsoluteUri ? uriResult.AbsolutePath : uriResult.ToString();
}
return string.Empty;
}
private JToken? MapOpenApiAnyToJToken(IOpenApiAny? any)
{
if (any == null)
{
return null;
}
using var outputString = new StringWriter();
var writer = new OpenApiJsonWriter(outputString);
any.Write(writer, OpenApiSpecVersion.OpenApi3_0);
if (any.AnyType == AnyType.Array)
{
return JArray.Parse(outputString.ToString());
}
return JObject.Parse(outputString.ToString());
}
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, OpenApiHeader>? headers)
{
var mappedHeaders = headers?.ToDictionary(
item => item.Key,
_ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!
) ?? new Dictionary<string, object>();
var mappedHeaders = headers?
.ToDictionary(item => item.Key, _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!) ?? new Dictionary<string, object>();
if (!string.IsNullOrEmpty(responseContentType))
{
mappedHeaders.TryAdd(HeaderContentType, responseContentType!);
mappedHeaders.TryAdd(HeaderContentType, responseContentType);
}
return mappedHeaders.Keys.Any() ? mappedHeaders : null;
}
private IList<ParamModel>? MapQueryParameters(IEnumerable<OpenApiParameter> queryParameters)
private IList<ParamModel>? MapQueryParameters(IEnumerable<IOpenApiParameter> queryParameters)
{
var list = queryParameters
.Where(req => req.Required)
.Select(qp => new ParamModel
{
Name = qp.Name,
Name = qp.Name ?? string.Empty,
IgnoreCase = _settings.QueryParameterPatternIgnoreCase,
Matchers = new[]
{
Matchers =
[
GetExampleMatcherModel(qp.Schema, _settings.QueryParameterPatternToUse)
}
]
})
.ToList();
return list.Any() ? list : null;
}
private IList<HeaderModel>? MapRequestHeaders(IEnumerable<OpenApiParameter> headers)
private IList<HeaderModel>? MapRequestHeaders(IEnumerable<IOpenApiParameter> headers)
{
var list = headers
.Where(req => req.Required)
.Select(qp => new HeaderModel
{
Name = qp.Name,
Name = qp.Name ?? string.Empty,
IgnoreCase = _settings.HeaderPatternIgnoreCase,
Matchers = new[]
{
Matchers =
[
GetExampleMatcherModel(qp.Schema, _settings.HeaderPatternToUse)
}
]
})
.ToList();
return list.Any() ? list : null;
}
private MatcherModel GetExampleMatcherModel(OpenApiSchema? schema, ExampleValueType type)
private MatcherModel GetExampleMatcherModel(IOpenApiSchema? schema, ExampleValueType type)
{
return type switch
{
@@ -385,15 +317,31 @@ internal class OpenApiPathsMapper
};
}
private string GetExampleValueAsStringForSchemaType(OpenApiSchema? schema)
private string GetExampleValueAsStringForSchemaType(IOpenApiSchema? schema)
{
var value = _exampleValueGenerator.GetExampleValue(schema);
return value switch
if (value.GetValueKind() == JsonValueKind.String)
{
string valueAsString => valueAsString,
return value.GetValue<string>();
}
_ => value.ToString(),
};
return value.ToString();
}
private static string MapBasePath(IList<OpenApiServer>? servers)
{
var server = servers?.FirstOrDefault();
if (server == null)
{
return string.Empty;
}
if (Uri.TryCreate(server.Url, UriKind.RelativeOrAbsolute, out var uriResult))
{
return uriResult.IsAbsoluteUri ? uriResult.AbsolutePath : uriResult.ToString();
}
return string.Empty;
}
}

View File

@@ -0,0 +1,5 @@
// Copyright © WireMock.Net
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]

View File

@@ -1,7 +1,7 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
namespace WireMock.Net.OpenApiParser.Settings;
@@ -26,9 +26,9 @@ public interface IWireMockOpenApiParserExampleValues
float Float { get; }
/// <summary>
/// An example value for a Double.
/// An example value for a Decimal.
/// </summary>
double Double { get; }
decimal Decimal { get; }
/// <summary>
/// An example value for a Date.
@@ -58,5 +58,5 @@ public interface IWireMockOpenApiParserExampleValues
/// <summary>
/// OpenApi Schema to generate dynamic examples more accurate
/// </summary>
OpenApiSchema? Schema { get; set; }
IOpenApiSchema? Schema { get; set; }
}

View File

@@ -1,7 +1,7 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using RandomDataGenerator.FieldOptions;
using RandomDataGenerator.Randomizers;
@@ -22,7 +22,7 @@ public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserE
public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f;
/// <inheritdoc />
public virtual double Double => RandomizerFactory.GetRandomizer(new FieldOptionsDouble()).Generate() ?? 4.2d;
public virtual decimal Decimal => SafeConvertFloatToDecimal(RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f);
/// <inheritdoc />
public virtual Func<DateTime> Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date;
@@ -40,5 +40,20 @@ public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserE
public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string";
/// <inheritdoc />
public virtual OpenApiSchema? Schema { get; set; }
public virtual IOpenApiSchema? Schema { get; set; }
/// <summary>
/// Safely converts a float to a decimal, ensuring the value stays within the bounds of a decimal.
/// </summary>
/// <param name="value">The float value to convert.</param>
/// <returns>A decimal value within the valid range of a decimal.</returns>
private static decimal SafeConvertFloatToDecimal(float value)
{
return value switch
{
< (float)decimal.MinValue => decimal.MinValue,
> (float)decimal.MaxValue => decimal.MaxValue,
_ => (decimal)value
};
}
}

View File

@@ -2,6 +2,7 @@
using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
namespace WireMock.Net.OpenApiParser.Settings;
@@ -20,7 +21,7 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
public virtual float Float => 4.2f;
/// <inheritdoc />
public virtual double Double => 4.2d;
public virtual decimal Decimal => 4.2m;
/// <inheritdoc />
public virtual Func<DateTime> Date { get; } = () => System.DateTime.UtcNow.Date;
@@ -29,7 +30,7 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
public virtual Func<DateTime> DateTime { get; } = () => System.DateTime.UtcNow;
/// <inheritdoc />
public virtual byte[] Bytes { get; } = { 48, 49, 50 };
public virtual byte[] Bytes { get; } = [48, 49, 50];
/// <inheritdoc />
public virtual object Object => "example-object";
@@ -38,5 +39,5 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
public virtual string String => "example-string";
/// <inheritdoc />
public virtual OpenApiSchema? Schema { get; set; } = new();
public virtual IOpenApiSchema? Schema { get; set; } = new OpenApiSchema();
}

View File

@@ -1,22 +0,0 @@
// Copyright © WireMock.Net
namespace WireMock.Net.OpenApiParser.Types;
internal enum SchemaType
{
Object,
Array,
String,
Integer,
Number,
Boolean,
File,
Unknown
}

View File

@@ -7,13 +7,16 @@ namespace WireMock.Net.OpenApiParser.Utils;
internal static class DateTimeUtils
{
private const string DateFormat = "yyyy-MM-dd";
private const string DateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.fffzzz";
public static string ToRfc3339DateTime(DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
return dateTime.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo);
}
public static string ToRfc3339Date(DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo);
return dateTime.ToString(DateFormat, DateTimeFormatInfo.InvariantInfo);
}
}

View File

@@ -1,8 +1,10 @@
// Copyright © WireMock.Net
using System;
using System.Linq;
using Microsoft.OpenApi.Any;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Stef.Validation;
using WireMock.Net.OpenApiParser.Extensions;
using WireMock.Net.OpenApiParser.Settings;
@@ -36,82 +38,66 @@ internal class ExampleValueGenerator
}
}
public object GetExampleValue(OpenApiSchema? schema)
public JsonNode GetExampleValue(IOpenApiSchema? schema)
{
var schemaExample = schema?.Example;
var schemaEnum = schema?.Enum?.FirstOrDefault();
_exampleValues.Schema = schema;
switch (schema?.GetSchemaType())
switch (schema?.GetSchemaType(out _))
{
case SchemaType.Boolean:
var exampleBoolean = schemaExample as OpenApiBoolean;
return exampleBoolean?.Value ?? _exampleValues.Boolean;
case JsonSchemaType.Boolean:
var exampleBoolean = schemaExample?.GetValue<bool>();
return exampleBoolean ?? _exampleValues.Boolean;
case SchemaType.Integer:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Int64:
var exampleLong = schemaExample as OpenApiLong;
var enumLong = schemaEnum as OpenApiLong;
var valueLongEnumOrExample = enumLong?.Value ?? exampleLong?.Value;
return valueLongEnumOrExample ?? _exampleValues.Integer;
case JsonSchemaType.Integer:
var exampleInteger = schemaExample?.GetValue<decimal>();
var enumInteger = schemaEnum?.GetValue<decimal>();
var valueIntegerEnumOrExample = enumInteger ?? exampleInteger;
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
default:
var exampleInteger = schemaExample as OpenApiInteger;
var enumInteger = schemaEnum as OpenApiInteger;
var valueIntegerEnumOrExample = enumInteger?.Value ?? exampleInteger?.Value;
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
}
case SchemaType.Number:
switch (schema?.GetSchemaFormat())
case JsonSchemaType.Number:
switch (schema.GetSchemaFormat())
{
case SchemaFormat.Float:
var exampleFloat = schemaExample as OpenApiFloat;
var enumFloat = schemaEnum as OpenApiFloat;
var valueFloatEnumOrExample = enumFloat?.Value ?? exampleFloat?.Value;
var exampleFloat = schemaExample?.GetValue<float>();
var enumFloat = schemaEnum?.GetValue<float>();
var valueFloatEnumOrExample = enumFloat ?? exampleFloat;
return valueFloatEnumOrExample ?? _exampleValues.Float;
default:
var exampleDouble = schemaExample as OpenApiDouble;
var enumDouble = schemaEnum as OpenApiDouble;
var valueDoubleEnumOrExample = enumDouble?.Value ?? exampleDouble?.Value;
return valueDoubleEnumOrExample ?? _exampleValues.Double;
var exampleDecimal = schemaExample?.GetValue<decimal>();
var enumDecimal = schemaEnum?.GetValue<decimal>();
var valueDecimalEnumOrExample = enumDecimal ?? exampleDecimal;
return valueDecimalEnumOrExample ?? _exampleValues.Decimal;
}
default:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Date:
var exampleDate = schemaExample as OpenApiDate;
var enumDate = schemaEnum as OpenApiDate;
var valueDateEnumOrExample = enumDate?.Value ?? exampleDate?.Value;
return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _exampleValues.Date());
var exampleDate = schemaExample?.GetValue<string>();
var enumDate = schemaEnum?.GetValue<string>();
var valueDateEnumOrExample = enumDate ?? exampleDate;
return valueDateEnumOrExample ?? DateTimeUtils.ToRfc3339Date(_exampleValues.Date());
case SchemaFormat.DateTime:
var exampleDateTime = schemaExample as OpenApiDateTime;
var enumDateTime = schemaEnum as OpenApiDateTime;
var valueDateTimeEnumOrExample = enumDateTime?.Value ?? exampleDateTime?.Value;
return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _exampleValues.DateTime());
var exampleDateTime = schemaExample?.GetValue<string>();
var enumDateTime = schemaEnum?.GetValue<string>();
var valueDateTimeEnumOrExample = enumDateTime ?? exampleDateTime;
return valueDateTimeEnumOrExample ?? DateTimeUtils.ToRfc3339DateTime(_exampleValues.DateTime());
case SchemaFormat.Byte:
var exampleByte = schemaExample as OpenApiByte;
var enumByte = schemaEnum as OpenApiByte;
var valueByteEnumOrExample = enumByte?.Value ?? exampleByte?.Value;
return valueByteEnumOrExample ?? _exampleValues.Bytes;
case SchemaFormat.Binary:
var exampleBinary = schemaExample as OpenApiBinary;
var enumBinary = schemaEnum as OpenApiBinary;
var valueBinaryEnumOrExample = enumBinary?.Value ?? exampleBinary?.Value;
return valueBinaryEnumOrExample ?? _exampleValues.Object;
var exampleByte = schemaExample?.GetValue<byte[]>();
var enumByte = schemaEnum?.GetValue<byte[]>();
var valueByteEnumOrExample = enumByte ?? exampleByte;
return Convert.ToBase64String(valueByteEnumOrExample ?? _exampleValues.Bytes);
default:
var exampleString = schemaExample as OpenApiString;
var enumString = schemaEnum as OpenApiString;
var valueStringEnumOrExample = enumString?.Value ?? exampleString?.Value;
var exampleString = schemaExample?.GetValue<string>();
var enumString = schemaEnum?.GetValue<string>();
var valueStringEnumOrExample = enumString ?? exampleString;
return valueStringEnumOrExample ?? _exampleValues.String;
}
}

View File

@@ -0,0 +1,27 @@
// Copyright © WireMock.Net
namespace WireMock.Net.OpenApiParser.Utils;
internal static class PathUtils
{
internal static string Combine(params string[] paths)
{
if (paths.Length == 0)
{
return string.Empty;
}
var result = paths[0].Trim().TrimEnd('/');
for (int i = 1; i < paths.Length; i++)
{
var nextPath = paths[i].Trim().TrimStart('/').TrimEnd('/');
if (!string.IsNullOrEmpty(nextPath))
{
result += '/' + nextPath;
}
}
return result;
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<Description>An OpenApi (swagger) parser to generate MappingModel or mapping.json file.</Description>
<TargetFrameworks>net46;netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net47;netstandard2.0;netstandard2.1;net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>wiremock;openapi;OAS;raml;converter;parser;openapiparser</PackageTags>
<ProjectGuid>{D3804228-91F4-4502-9595-39584E5AADAD}</ProjectGuid>
@@ -20,12 +20,11 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.2.3" />
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RamlToOpenApiConverter" Version="0.6.1" />
<PackageReference Include="RamlToOpenApiConverter" Version="0.7.0" />
<PackageReference Include="RandomDataGenerator.Net" Version="1.0.18" />
<PackageReference Include="Stef.Validation" Version="0.1.1" />
</ItemGroup>

View File

@@ -6,7 +6,8 @@ using System.IO;
using System.Text;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Microsoft.OpenApi.Reader;
using Microsoft.OpenApi.YamlReader;
using RamlToOpenApiConverter;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Mappers;
@@ -19,7 +20,7 @@ namespace WireMock.Net.OpenApiParser;
/// </summary>
public class WireMockOpenApiParser : IWireMockOpenApiParser
{
private readonly OpenApiStreamReader _reader = new();
private static readonly OpenApiReaderSettings ReaderSettings = new();
/// <inheritdoc />
[PublicAPI]
@@ -40,8 +41,7 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
}
else
{
var reader = new OpenApiStreamReader();
document = reader.Read(File.OpenRead(path), out diagnostic);
document = Read(File.OpenRead(path), out diagnostic);
}
return FromDocument(document, settings);
@@ -51,21 +51,21 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
[PublicAPI]
public IReadOnlyList<MappingModel> FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null)
{
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers);
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers ?? []);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic)
{
return FromDocument(_reader.Read(stream, out diagnostic));
return FromDocument(Read(stream, out diagnostic));
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromDocument(_reader.Read(stream, out diagnostic), settings);
return FromDocument(Read(stream, out diagnostic), settings);
}
/// <inheritdoc />
@@ -81,4 +81,27 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
{
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic);
}
private static OpenApiDocument Read(Stream stream, out OpenApiDiagnostic diagnostic)
{
var reader = new OpenApiYamlReader();
if (stream is not MemoryStream memoryStream)
{
memoryStream = ReadStreamIntoMemoryStream(stream);
}
var result = reader.Read(memoryStream, ReaderSettings);
diagnostic = result.Diagnostic ?? new OpenApiDiagnostic();
return result.Document ?? throw new InvalidOperationException("The document is null.");
}
private static MemoryStream ReadStreamIntoMemoryStream(Stream stream)
{
var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
memoryStream.Position = 0;
return memoryStream;
}
}

View File

@@ -0,0 +1,31 @@
using System.Globalization;
namespace WireMock.Extensions;
internal static class StringExtensions
{
// See https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
public static string GetDeterministicHashCodeAsString(this string str)
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < str.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1)
{
break;
}
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
int result = hash1 + hash2 * 1566083941;
return result.ToString(CultureInfo.InvariantCulture).Replace('-', '_');
}
}
}

View File

@@ -84,20 +84,20 @@ public class LocalFileSystemHandler : IFileSystemHandler
public virtual byte[] ReadResponseBodyAsFile(string path)
{
Guard.NotNullOrEmpty(path);
path = PathUtils.CleanPath(path)!;
path = FilePathUtils.CleanPath(path)!;
// If the file exists at the given path relative to the MappingsFolder, then return that.
// Else the path will just be as-is.
return File.ReadAllBytes(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
return File.ReadAllBytes(File.Exists(FilePathUtils.Combine(GetMappingFolder(), path)) ? FilePathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsString"/>
public virtual string ReadResponseBodyAsString(string path)
{
Guard.NotNullOrEmpty(path);
path = PathUtils.CleanPath(path)!;
path = FilePathUtils.CleanPath(path)!;
// In case the path is a filename, the path will be adjusted to the MappingFolder.
// Else the path will just be as-is.
return File.ReadAllText(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
return File.ReadAllText(File.Exists(FilePathUtils.Combine(GetMappingFolder(), path)) ? FilePathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.FileExists"/>
@@ -124,7 +124,7 @@ public class LocalFileSystemHandler : IFileSystemHandler
Guard.NotNullOrEmpty(filename);
Guard.NotNull(bytes);
File.WriteAllBytes(PathUtils.Combine(folder, filename), bytes);
File.WriteAllBytes(FilePathUtils.Combine(folder, filename), bytes);
}
/// <inheritdoc cref="IFileSystemHandler.DeleteFile"/>

View File

@@ -0,0 +1,85 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
namespace WireMock.Models;
/// <inheritdoc />
internal class BlockingQueue<T>(TimeSpan? readTimeout = null) : IBlockingQueue<T>
{
private readonly TimeSpan _readTimeout = readTimeout ?? TimeSpan.FromHours(1);
private readonly Queue<T?> _queue = new();
private readonly object _lockObject = new();
private bool _isClosed;
/// <summary>
/// Writes an item to the queue and signals that an item is available.
/// </summary>
/// <param name="item">The item to be added to the queue.</param>
public void Write(T item)
{
lock (_lockObject)
{
if (_isClosed)
{
throw new InvalidOperationException("Cannot write to a closed queue.");
}
_queue.Enqueue(item);
// Signal that an item is available
Monitor.Pulse(_lockObject);
}
}
/// <summary>
/// Tries to read an item from the queue.
/// - waits until an item is available
/// - or the timeout occurs
/// - or queue is closed
/// </summary>
/// <param name="item">The item read from the queue, or default if the timeout occurs.</param>
/// <returns>True if an item was successfully read; otherwise, false.</returns>
public bool TryRead([NotNullWhen(true)] out T? item)
{
lock (_lockObject)
{
// Wait until an item is available or timeout occurs
while (_queue.Count == 0 && !_isClosed)
{
// Wait with timeout
if (!Monitor.Wait(_lockObject, _readTimeout))
{
item = default;
return false;
}
}
// After waiting, check if we have items
if (_queue.Count == 0)
{
item = default;
return false;
}
item = _queue.Dequeue();
return item != null;
}
}
/// <summary>
/// Closes the queue and signals all waiting threads.
/// </summary>
public void Close()
{
lock (_lockObject)
{
_isClosed = true;
Monitor.PulseAll(_lockObject);
}
}
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using WireMock.Models;
using WireMock.Types;
@@ -57,4 +58,10 @@ public class BodyData : IBodyData
/// <inheritdoc />
public string? ProtoBufMessageType { get; set; }
#endregion
/// <inheritdoc />
public IBlockingQueue<string?>? SseStringQueue { get; set; }
/// <inheritdoc />
public Task? BodyAsSseStringTask { get; set; }
}

View File

@@ -0,0 +1,39 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
namespace WireMock.Models;
/// <summary>
/// A placeholder class for Proto Definitions.
/// </summary>
public class ProtoDefinitionData
{
private readonly IDictionary<string, string> _filenameMappedToProtoDefinition;
internal ProtoDefinitionData(IDictionary<string, string> filenameMappedToProtoDefinition)
{
_filenameMappedToProtoDefinition = filenameMappedToProtoDefinition;
}
/// <summary>
/// Get all the ProtoDefinitions.
/// Note: the main ProtoDefinition will be the first one in the list.
/// </summary>
/// <param name="mainProtoFilename">The main ProtoDefinition filename.</param>
public IReadOnlyList<string> ToList(string mainProtoFilename)
{
Guard.NotNullOrEmpty(mainProtoFilename);
if (!_filenameMappedToProtoDefinition.TryGetValue(mainProtoFilename, out var mainProtoDefinition))
{
throw new KeyNotFoundException($"The ProtoDefinition with filename '{mainProtoFilename}' was not found.");
}
var list = new List<string> { mainProtoDefinition };
list.AddRange(_filenameMappedToProtoDefinition.Where(kvp => kvp.Key != mainProtoFilename).Select(kvp => kvp.Value));
return list;
}
}

View File

@@ -69,6 +69,13 @@ namespace WireMock.Owin.Mappers
return;
}
var bodyData = responseMessage.BodyData;
if (bodyData?.GetBodyType() == BodyType.SseString)
{
await HandleSseStringAsync(responseMessage, response, bodyData);
return;
}
byte[]? bytes;
switch (responseMessage.FaultType)
{
@@ -104,7 +111,7 @@ namespace WireMock.Owin.Mappers
}
}
SetResponseHeaders(responseMessage, bytes, response);
SetResponseHeaders(responseMessage, bytes != null, response);
if (bytes != null)
{
@@ -121,6 +128,26 @@ namespace WireMock.Owin.Mappers
SetResponseTrailingHeaders(responseMessage, response);
}
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, IResponse response, IBodyData bodyData)
{
if (bodyData.SseStringQueue == null)
{
return;
}
SetResponseHeaders(responseMessage, true, response);
string? text;
do
{
if (bodyData.SseStringQueue.TryRead(out text))
{
await response.WriteAsync(text);
await response.Body.FlushAsync();
}
} while (text != null);
}
private int MapStatusCode(int code)
{
if (_options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
@@ -136,7 +163,8 @@ namespace WireMock.Owin.Mappers
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
}
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage) {
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
{
var bodyData = responseMessage.BodyData;
switch (bodyData?.GetBodyType())
{
@@ -172,13 +200,13 @@ namespace WireMock.Owin.Mappers
return null;
}
private static void SetResponseHeaders(IResponseMessage responseMessage, byte[]? bytes, IResponse response)
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, IResponse response)
{
// Force setting the Date header (#577)
AppendResponseHeader(
response,
HttpKnownHeaderNames.Date,
[ DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture) ]
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
);
// Set other headers
@@ -188,7 +216,7 @@ namespace WireMock.Owin.Mappers
var value = item.Value;
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
{
action?.Invoke(response, bytes != null, value);
action?.Invoke(response, hasBody, value);
}
else
{

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using WireMock.Matchers;
using WireMock.Matchers.Request;
@@ -12,7 +13,7 @@ public partial class Request
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsProtoBuf([ protoDefinition ], messageType, matchBehaviour);
return WithBodyAsProtoBuf([protoDefinition], messageType, matchBehaviour);
}
/// <inheritdoc />
@@ -36,12 +37,25 @@ public partial class Request
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType));
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType, matcher));
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType, matcher));
}
private Func<IdOrTexts> ProtoDefinitionFunc()
{
return () =>
{
if (Mapping.ProtoDefinition == null)
{
throw new InvalidOperationException($"No ProtoDefinition defined on mapping '{Mapping.Guid}'. Please use the WireMockServerSettings to define ProtoDefinitions.");
}
return Mapping.ProtoDefinition.Value;
};
}
}

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JsonConverter.Abstractions;
using WireMock.Models;
namespace WireMock.ResponseBuilders;
@@ -32,7 +33,7 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
IResponseBuilder WithBody(Func<IRequestMessage, string> bodyFactory, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null);
/// <summary>
/// WithBody : Create a ... response based on a async callback function.
/// WithBody : Create a ... response based on an async callback function.
/// </summary>
/// <param name="bodyFactory">The async delegate to build the body.</param>
/// <param name="destination">The Body Destination format (SameAsSource, String or Bytes).</param>
@@ -40,6 +41,14 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBody(Func<IRequestMessage, Task<string>> bodyFactory, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null);
/// <summary>
/// WithBody : Create a ... response based on an async callback function.
/// </summary>
/// <param name="bodyFactory">The async delegate to build the body.</param>
/// <param name="timeout">The timeout to wait on new items in the queue. Default value is <c>1</c> hour.</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithSseBody(Func<IRequestMessage, IBlockingQueue<string?>, Task> bodyFactory, TimeSpan? timeout = null);
/// <summary>
/// WithBody : Create a ... response based on a bytearray.
/// </summary>

View File

@@ -51,6 +51,26 @@ public partial class Response
});
}
/// <inheritdoc />
public IResponseBuilder WithSseBody(Func<IRequestMessage, IBlockingQueue<string?>, Task> bodyFactory, TimeSpan? timeout = null)
{
Guard.NotNull(bodyFactory);
var queue = new BlockingQueue<string?>(timeout);
return WithCallbackInternal(true, req => new ResponseMessage
{
BodyData = new BodyData
{
DetectedBodyType = BodyType.SseString,
SseStringQueue = queue,
BodyAsSseStringTask = bodyFactory(req, queue),
Encoding = Encoding.UTF8,
IsFuncUsed = "Func<IRequestMessage, BlockingQueue<string?>, Task>"
}
});
}
/// <inheritdoc />
public IResponseBuilder WithBody(byte[] body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null)
{

View File

@@ -356,9 +356,12 @@ internal class RespondWithAProvider : IRespondWithAProvider
{
Guard.NotNull(protoDefinitionOrId);
#if PROTOBUF
ProtoDefinition = ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitionOrId);
return this;
#else
throw new NotSupportedException("The WithProtoDefinition method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
#endif
}
/// <inheritdoc />

View File

@@ -25,7 +25,7 @@ public partial class WireMockServer
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, e.Message);
}
#else
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.");
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.6.x or lower.");
#endif
}
@@ -50,7 +50,7 @@ public partial class WireMockServer
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, e.Message);
}
#else
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.");
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.6.x or lower.");
#endif
}
}

View File

@@ -0,0 +1,86 @@
// Copyright © WireMock.Net
using System.IO;
using Stef.Validation;
namespace WireMock.Util;
internal static class FilePathUtils
{
/// <summary>
/// Robust handling of the user defined path.
/// Also supports Unix and Windows platforms
/// </summary>
/// <param name="path">The path to clean</param>
public static string? CleanPath(string? path)
{
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
}
/// <summary>
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
/// </summary>
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
public static string? RemoveLeadingDirectorySeparators(string? path)
{
return path?.TrimStart(Path.DirectorySeparatorChar);
}
/// <summary>
/// Combine two paths
/// </summary>
/// <param name="root">The root path</param>
/// <param name="path">The path</param>
public static string Combine(string root, string? path)
{
Guard.NotNull(root);
var result = RemoveLeadingDirectorySeparators(path);
return result == null ? root : Path.Combine(root, result);
}
/// <summary>
/// Returns a relative path from one path to another.
/// </summary>
/// <param name="relativeTo">The source path the result should be relative to. This path is always considered to be a directory..</param>
/// <param name="path">The destination path.</param>
/// <returns>The relative path, or path if the paths don't share the same root.</returns>
public static string GetRelativePath(string relativeTo, string path)
{
#if NETCOREAPP3_1 || NET5_0_OR_GREATER || NETSTANDARD2_1
return Path.GetRelativePath(relativeTo, path);
#else
Guard.NotNull(relativeTo);
Guard.NotNull(path);
static string AppendDirectorySeparatorChar(string path)
{
// Append a slash only if the path is a directory and does not have a slash.
if (!Path.HasExtension(path) && !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
return path + Path.DirectorySeparatorChar;
}
return path;
}
var fromUri = new System.Uri(AppendDirectorySeparatorChar(relativeTo));
var toUri = new System.Uri(AppendDirectorySeparatorChar(path));
if (fromUri.Scheme != toUri.Scheme)
{
return path;
}
var relativeUri = fromUri.MakeRelativeUri(toUri);
var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString());
if (string.Equals(toUri.Scheme, "FILE", System.StringComparison.OrdinalIgnoreCase))
{
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
return relativePath;
#endif
}
}

View File

@@ -1,41 +0,0 @@
// Copyright © WireMock.Net
using System.IO;
using Stef.Validation;
namespace WireMock.Util;
internal static class PathUtils
{
/// <summary>
/// Robust handling of the user defined path.
/// Also supports Unix and Windows platforms
/// </summary>
/// <param name="path">The path to clean</param>
public static string? CleanPath(string? path)
{
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
}
/// <summary>
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
/// </summary>
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
public static string? RemoveLeadingDirectorySeparators(string? path)
{
return path?.TrimStart(new[] { Path.DirectorySeparatorChar });
}
/// <summary>
/// Combine two paths
/// </summary>
/// <param name="root">The root path</param>
/// <param name="path">The path</param>
public static string Combine(string root, string? path)
{
Guard.NotNull(root);
var result = RemoveLeadingDirectorySeparators(path);
return result == null ? root : Path.Combine(root, result);
}
}

View File

@@ -1,12 +1,85 @@
// Copyright © WireMock.Net
#if PROTOBUF
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ProtoBufJsonConverter;
using ProtoBufJsonConverter.Models;
using Stef.Validation;
using WireMock.Models;
using WireMock.Settings;
namespace WireMock.Util;
internal static class ProtoDefinitionHelper
/// <summary>
/// Some helper methods for Proto Definitions.
/// </summary>
public static class ProtoDefinitionHelper
{
/// <summary>
/// Builds a dictionary of ProtoDefinitions from a directory.
/// - The key will be the filename without extension.
/// - The value will be the ProtoDefinition with an extra comment with the relative path to each <c>.proto</c> file so it can be used by the WireMockProtoFileResolver.
/// </summary>
/// <param name="directory">The directory to start from.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>System.Threading.CancellationToken.None</c>.</param>
public static async Task<ProtoDefinitionData> FromDirectory(string directory, CancellationToken cancellationToken = default)
{
Guard.NotNullOrEmpty(directory);
var fileNameMappedToProtoDefinition = new Dictionary<string, string>();
var filePaths = Directory.EnumerateFiles(directory, "*.proto", SearchOption.AllDirectories);
foreach (var filePath in filePaths)
{
// Get the relative path to the directory (note that this will be OS specific).
var relativePath = FilePathUtils.GetRelativePath(directory, filePath);
// Make it a valid proto import path
var protoRelativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
// Build comment and get content from file.
var comment = $"// {protoRelativePath}";
#if NETSTANDARD2_0
var content = File.ReadAllText(filePath);
#else
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
#endif
// Only add the comment if it's not already defined.
var modifiedContent = !content.StartsWith(comment) ? $"{comment}\n{content}" : content;
var key = Path.GetFileNameWithoutExtension(filePath);
fileNameMappedToProtoDefinition.Add(key, modifiedContent);
}
var converter = SingletonFactory<Converter>.GetInstance();
var resolver = new WireMockProtoFileResolver(fileNameMappedToProtoDefinition.Values);
var messageTypeMappedToWithProtoDefinition = new Dictionary<string, string>();
foreach (var protoDefinition in fileNameMappedToProtoDefinition.Values)
{
var infoRequest = new GetInformationRequest(protoDefinition, resolver);
try
{
var info = await converter.GetInformationAsync(infoRequest, cancellationToken);
foreach (var messageType in info.MessageTypes)
{
messageTypeMappedToWithProtoDefinition[messageType.Key] = protoDefinition;
}
}
catch
{
// Ignore
}
}
return new ProtoDefinitionData(fileNameMappedToProtoDefinition);
}
internal static IdOrTexts GetIdOrTexts(WireMockServerSettings settings, params string[] protoDefinitionOrId)
{
switch (protoDefinitionOrId.Length)
@@ -19,9 +92,10 @@ internal static class ProtoDefinitionHelper
}
return new(null, protoDefinitionOrId);
default:
return new(null, protoDefinitionOrId);
}
}
}
}
#endif

View File

@@ -7,18 +7,19 @@ using System.IO;
using System.Linq;
using ProtoBufJsonConverter;
using Stef.Validation;
using WireMock.Extensions;
namespace WireMock.Util;
/// <summary>
/// This resolver is used to resolve the extra ProtoDefinition files.
///
/// It assumes that:
/// - the first ProtoDefinition file is the main ProtoDefinition file.
/// - the first commented line of each extra ProtoDefinition file is the filename which is used in the import of the other ProtoDefinition file(s).
/// - The first commented line of each ProtoDefinition file is the filepath which is used in the import of the other ProtoDefinition file(s).
/// </summary>
internal class WireMockProtoFileResolver : IProtoFileResolver
{
private readonly Dictionary<string, string> _files = new();
private readonly Dictionary<string, string> _files = [];
public WireMockProtoFileResolver(IReadOnlyCollection<string> protoDefinitions)
{
@@ -27,12 +28,19 @@ internal class WireMockProtoFileResolver : IProtoFileResolver
return;
}
foreach (var extraProtoDefinition in protoDefinitions.Skip(1))
foreach (var extraProtoDefinition in protoDefinitions)
{
var firstNonEmptyLine = extraProtoDefinition.Split(['\r', '\n']).FirstOrDefault(l => !string.IsNullOrEmpty(l));
if (firstNonEmptyLine != null && TryGetValidFileName(firstNonEmptyLine.TrimStart(['/', ' ']), out var validFileName))
if (firstNonEmptyLine != null)
{
_files.Add(validFileName, extraProtoDefinition);
if (TryGetValidPath(firstNonEmptyLine.TrimStart(['/', ' ']), out var validPath))
{
_files.Add(validPath, extraProtoDefinition);
}
else
{
_files.Add(extraProtoDefinition.GetDeterministicHashCodeAsString(), extraProtoDefinition);
}
}
}
}
@@ -52,15 +60,15 @@ internal class WireMockProtoFileResolver : IProtoFileResolver
throw new FileNotFoundException($"The ProtoDefinition '{path}' was not found.");
}
private static bool TryGetValidFileName(string fileName, [NotNullWhen(true)] out string? validFileName)
private static bool TryGetValidPath(string path, [NotNullWhen(true)] out string? validPath)
{
if (!fileName.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
if (!path.Any(c => Path.GetInvalidPathChars().Contains(c)))
{
validFileName = fileName;
validPath = path;
return true;
}
validFileName = null;
validPath = null;
return false;
}
}

View File

@@ -46,7 +46,7 @@
<DefineConstants>$(DefineConstants);USE_ASPNETCORE;NET46</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
<DefineConstants>$(DefineConstants);OPENAPIPARSER</DefineConstants>
</PropertyGroup>
@@ -195,17 +195,17 @@
<PackageReference Include="Handlebars.Net.Helpers.Xslt" Version="2.4.6" />
</ItemGroup>
<ItemGroup>
<!-- CVE-2021-26701 and https://github.com/WireMock-Net/WireMock.Net/issues/697 -->
<!--<ItemGroup>
--><!-- CVE-2021-26701 and https://github.com/WireMock-Net/WireMock.Net/issues/697 --><!--
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
</ItemGroup>
</ItemGroup>-->
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
<ProjectReference Include="..\WireMock.Org.Abstractions\WireMock.Org.Abstractions.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
<ProjectReference Include="..\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj" />
</ItemGroup>
</Project>