diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 0438f830..d7c71918 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -46,8 +46,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Console.Net452 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.RequestLogTest", "examples\WireMock.Net.Console.RequestLogTest\WireMock.Net.Console.RequestLogTest.csproj", "{A9D039B9-7509-4CF1-9EFD-87EB82998575}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.OpenApiParser", "src\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj", "{D3804228-91F4-4502-9595-39584E5AADAD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.OpenApiParser.ConsoleApp", "examples\WireMock.Net.OpenApiParser.ConsoleApp\WireMock.Net.OpenApiParser.ConsoleApp.csproj", "{5C09FB93-1535-4F92-AF26-21E8A061EE4A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.FluentAssertions", "src\WireMock.Net.FluentAssertions\WireMock.Net.FluentAssertions.csproj", "{B6269AAC-170A-4346-8B9A-579DED3D9A95}" @@ -130,6 +128,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Middleware.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.AwesomeAssertions", "src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj", "{7753670F-7C7F-44BF-8BC7-08325588E60C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.OpenApiParser", "src\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj", "{D3804228-91F4-4502-9595-39584E5AADAD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -176,10 +176,6 @@ Global {A9D039B9-7509-4CF1-9EFD-87EB82998575}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9D039B9-7509-4CF1-9EFD-87EB82998575}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9D039B9-7509-4CF1-9EFD-87EB82998575}.Release|Any CPU.Build.0 = Release|Any CPU - {D3804228-91F4-4502-9595-39584E5AADAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3804228-91F4-4502-9595-39584E5AADAD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3804228-91F4-4502-9595-39584E5AADAD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3804228-91F4-4502-9595-39584E5AADAD}.Release|Any CPU.Build.0 = Release|Any CPU {5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -308,6 +304,10 @@ Global {7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.Build.0 = Debug|Any CPU {7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.Build.0 = Release|Any CPU + {D3804228-91F4-4502-9595-39584E5AADAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3804228-91F4-4502-9595-39584E5AADAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3804228-91F4-4502-9595-39584E5AADAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3804228-91F4-4502-9595-39584E5AADAD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -323,7 +323,6 @@ Global {7F0B2446-0363-4720-AF46-F47F83B557DC} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {668F689E-57B4-422E-8846-C0FF643CA268} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {A9D039B9-7509-4CF1-9EFD-87EB82998575} = {985E0ADB-D4B4-473A-AA40-567E279B7946} - {D3804228-91F4-4502-9595-39584E5AADAD} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {5C09FB93-1535-4F92-AF26-21E8A061EE4A} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {B6269AAC-170A-4346-8B9A-579DED3D9A95} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {40BF24B5-12E6-4610-9489-138798632E28} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} @@ -358,6 +357,7 @@ Global {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE} = {0BB8B634-407A-4610-A91F-11586990767A} {A5FEF4F7-7DA2-4962-89A8-16BA942886E5} = {0BB8B634-407A-4610-A91F-11586990767A} {7753670F-7C7F-44BF-8BC7-08325588E60C} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} + {D3804228-91F4-4502-9595-39584E5AADAD} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/examples/WireMock.Net.OpenApiParser.ConsoleApp/WireMock.Net.OpenApiParser.ConsoleApp.csproj b/examples/WireMock.Net.OpenApiParser.ConsoleApp/WireMock.Net.OpenApiParser.ConsoleApp.csproj index b63d3343..ddc6cf32 100644 --- a/examples/WireMock.Net.OpenApiParser.ConsoleApp/WireMock.Net.OpenApiParser.ConsoleApp.csproj +++ b/examples/WireMock.Net.OpenApiParser.ConsoleApp/WireMock.Net.OpenApiParser.ConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 diff --git a/src/WireMock.Net.OpenApiParser.Preview/Extensions/DictionaryExtensions.cs b/src/WireMock.Net.OpenApiParser.Preview/Extensions/DictionaryExtensions.cs new file mode 100644 index 00000000..a819a1f9 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Extensions/DictionaryExtensions.cs @@ -0,0 +1,22 @@ +// Copyright © WireMock.Net + +#if NET46 || NET47 || NETSTANDARD2_0 +using System.Collections.Generic; + +namespace WireMock.Net.OpenApiParser.Extensions; + +internal static class DictionaryExtensions +{ + public static bool TryAdd(this Dictionary? dictionary, TKey key, TValue value) + { + if (dictionary is null || dictionary.ContainsKey(key)) + { + return false; + } + + dictionary[key] = value; + + return true; + } +} +#endif \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Extensions/OpenApiSchemaExtensions.cs b/src/WireMock.Net.OpenApiParser.Preview/Extensions/OpenApiSchemaExtensions.cs new file mode 100644 index 00000000..02f7a49b --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Extensions/OpenApiSchemaExtensions.cs @@ -0,0 +1,85 @@ +// Copyright © WireMock.Net + +using System.Linq; +using System.Text.Json; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; +using WireMock.Net.OpenApiParser.Types; + +namespace WireMock.Net.OpenApiParser.Extensions; + +internal static class OpenApiSchemaExtensions +{ + public static bool TryGetXNullable(this IOpenApiSchema schema, out bool value) + { + value = false; + + if (schema.Extensions != null && schema.Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny { Node: { } jsonNode }) + { + value = jsonNode.GetValueKind() == JsonValueKind.True; + return true; + } + + return false; + } + + public static JsonSchemaType? GetSchemaType(this IOpenApiSchema? schema, out bool isNullable) + { + isNullable = false; + + if (schema == null) + { + return null; + } + + if (schema.Type == null) + { + if (schema.AllOf?.Any() == true || schema.AnyOf?.Any() == true) + { + return JsonSchemaType.Object; + } + } + + 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 IOpenApiSchema? schema) + { + switch (schema?.Format) + { + case "float": + return SchemaFormat.Float; + + case "double": + return SchemaFormat.Double; + + case "int32": + return SchemaFormat.Int32; + + case "int64": + return SchemaFormat.Int64; + + case "date": + return SchemaFormat.Date; + + case "date-time": + return SchemaFormat.DateTime; + + case "password": + return SchemaFormat.Password; + + case "byte": + return SchemaFormat.Byte; + + case "binary": + return SchemaFormat.Binary; + + default: + return SchemaFormat.Undefined; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Extensions/WireMockServerExtensions.cs b/src/WireMock.Net.OpenApiParser.Preview/Extensions/WireMockServerExtensions.cs new file mode 100644 index 00000000..8d8aee7f --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Extensions/WireMockServerExtensions.cs @@ -0,0 +1,96 @@ +// Copyright © WireMock.Net + +using System.IO; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader; +using Stef.Validation; +using WireMock.Net.OpenApiParser.Settings; +using WireMock.Server; + +namespace WireMock.Net.OpenApiParser.Extensions; + +/// +/// Some extension methods for . +/// +public static class WireMockServerExtensions +{ + /// + /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file. + /// + /// The WireMockServer instance + /// Path containing OpenAPI file to parse and use the mappings. + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, out OpenApiDiagnostic diagnostic) + { + return WithMappingFromOpenApiFile(server, path, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file. + /// + /// The WireMockServer instance + /// Path containing OpenAPI file to parse and use the mappings. + /// Additional settings + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + Guard.NotNull(server); + Guard.NotNullOrEmpty(path); + + var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic); + + return server.WithMapping(mappings.ToArray()); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream. + /// + /// The WireMockServer instance + /// Stream containing OpenAPI description to parse and use the mappings. + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, out OpenApiDiagnostic diagnostic) + { + return WithMappingFromOpenApiStream(server, stream, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream. + /// + /// The WireMockServer instance + /// Stream containing OpenAPI description to parse and use the mappings. + /// Additional settings + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + Guard.NotNull(server); + Guard.NotNull(stream); + Guard.NotNull(settings); + + var mappings = new WireMockOpenApiParser().FromStream(stream, settings, out diagnostic); + + return server.WithMapping(mappings.ToArray()); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 document. + /// + /// The WireMockServer instance + /// The OpenAPI document to use as mappings. + /// Additional settings [optional]. + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) + { + Guard.NotNull(server); + Guard.NotNull(document); + + var mappings = new WireMockOpenApiParser().FromDocument(document, settings); + + return server.WithMapping(mappings.ToArray()); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/IWireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser.Preview/IWireMockOpenApiParser.cs new file mode 100644 index 00000000..4e978739 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/IWireMockOpenApiParser.cs @@ -0,0 +1,75 @@ +// Copyright © WireMock.Net + +using System.Collections.Generic; +using System.IO; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader; +using WireMock.Admin.Mappings; +using WireMock.Net.OpenApiParser.Settings; + +namespace WireMock.Net.OpenApiParser; + +/// +/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. +/// +public interface IWireMockOpenApiParser +{ + /// + /// Generate from a file-path. + /// + /// The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file. + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a file-path. + /// + /// The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file. + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from an . + /// + /// The source OpenApiDocument + /// Additional settings [optional] + /// MappingModel + IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null); + + /// + /// Generate from a . + /// + /// The source stream + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source stream + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source text + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source text + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Mappers/OpenApiPathsMapper.cs b/src/WireMock.Net.OpenApiParser.Preview/Mappers/OpenApiPathsMapper.cs new file mode 100644 index 00000000..e48766e0 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Mappers/OpenApiPathsMapper.cs @@ -0,0 +1,347 @@ +// Copyright © WireMock.Net + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; +using Newtonsoft.Json; +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; + +internal class OpenApiPathsMapper +{ + private const string HeaderContentType = "Content-Type"; + + private readonly WireMockOpenApiParserSettings _settings; + private readonly ExampleValueGenerator _exampleValueGenerator; + + public OpenApiPathsMapper(WireMockOpenApiParserSettings settings) + { + _settings = Guard.NotNull(settings); + _exampleValueGenerator = new ExampleValueGenerator(settings); + } + + public IReadOnlyList ToMappingModels(OpenApiPaths? paths, IList servers) + { + return paths? + .OrderBy(p => p.Key) + .Select(p => MapPath(p.Key, p.Value, servers)) + .SelectMany(x => x) + .ToArray() ?? []; + } + + private IReadOnlyList MapPath(string path, IOpenApiPathItem pathItem, IList servers) + { + 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 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 response = operation?.Responses?.FirstOrDefault() ?? new KeyValuePair(); + + 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 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 var requestContent, out _); + + var requestBodySchema = operation.RequestBody.Content.First().Value?.Schema; + var requestBodyExample = requestContent!.Example; + var requestBodySchemaExample = requestContent.Schema?.Example; + + var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema); + requestBodyModel = MapRequestBody(requestBodyMapped); + } + + if (!int.TryParse(response.Key, out var httpStatusCode)) + { + httpStatusCode = 200; + } + + return new MappingModel + { + Guid = Guid.NewGuid(), + Request = new RequestModel + { + Methods = [httpMethod], + Path = PathUtils.Combine(MapBasePath(servers), MapPathWithParameters(path, pathParameters)), + Params = MapQueryParameters(queryParameters), + Headers = MapRequestHeaders(headers), + Body = requestBodyModel + }, + Response = new ResponseModel + { + StatusCode = httpStatusCode, + Headers = MapHeaders(responseContentType, response.Value?.Headers), + BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null + } + }; + } + + private BodyModel? MapRequestBody(JsonNode? requestBody) + { + if (requestBody == null) + { + return null; + } + + return new BodyModel + { + Matcher = new MatcherModel + { + Name = "JsonMatcher", + Pattern = SystemTextJsonSerializer.Serialize(requestBody, new JsonSerializerOptions { WriteIndented = true }), + IgnoreCase = _settings.RequestBodyIgnoreCase + } + }; + } + + private static bool TryGetContent(IDictionary? contents, [NotNullWhen(true)] out OpenApiMediaType? openApiMediaType, [NotNullWhen(true)] out string? contentType) + { + openApiMediaType = null; + contentType = null; + + if (contents == null || contents.Values.Count == 0) + { + return false; + } + + if (contents.TryGetValue("application/json", out var content)) + { + openApiMediaType = content; + contentType = "application/json"; + } + else + { + var first = contents.FirstOrDefault(); + openApiMediaType = first.Value; + contentType = first.Key; + } + + return true; + } + + private JsonNode? MapSchemaToObject(IOpenApiSchema? schema) + { + if (schema == null) + { + return null; + } + + switch (schema.GetSchemaType(out _)) + { + case JsonSchemaType.Array: + var array = new JsonArray(); + for (var i = 0; i < _settings.NumberOfArrayItems; i++) + { + if (schema.Items?.Properties?.Count > 0) + { + var item = new JsonObject(); + foreach (var property in schema.Items.Properties) + { + item[property.Key] = MapSchemaToObject(property.Value); + } + + array.Add(item); + } + else + { + var arrayItem = MapSchemaToObject(schema.Items); + array.Add(arrayItem); + } + } + + if (schema.AllOf?.Count > 0) + { + array.Add(MapSchemaAllOfToObject(schema)); + } + + return array; + + case JsonSchemaType.Boolean: + case JsonSchemaType.Integer: + case JsonSchemaType.Number: + case JsonSchemaType.String: + return _exampleValueGenerator.GetExampleValue(schema); + + case JsonSchemaType.Object: + var propertyAsJsonObject = new JsonObject(); + foreach (var schemaProperty in schema.Properties ?? new Dictionary()) + { + propertyAsJsonObject[schemaProperty.Key] = MapPropertyAsJsonNode(schemaProperty.Value); + } + + if (schema.AllOf?.Count > 0) + { + foreach (var group in schema.AllOf.SelectMany(p => p.Properties ?? new Dictionary()).GroupBy(x => x.Key)) + { + propertyAsJsonObject[group.Key] = MapPropertyAsJsonNode(group.First().Value); + } + } + + return propertyAsJsonObject; + + default: + return null; + } + } + + private JsonObject MapSchemaAllOfToObject(IOpenApiSchema schema) + { + var arrayItem = new JsonObject(); + foreach (var property in schema.AllOf ?? []) + { + foreach (var item in property.Properties ?? new Dictionary()) + { + arrayItem[item.Key] = MapPropertyAsJsonNode(item.Value); + } + } + return arrayItem; + } + + private JsonNode? MapPropertyAsJsonNode(IOpenApiSchema openApiSchema) + { + var schemaType = openApiSchema.GetSchemaType(out _); + if (schemaType is JsonSchemaType.Object or JsonSchemaType.Array) + { + return MapSchemaToObject(openApiSchema); + } + + return _exampleValueGenerator.GetExampleValue(openApiSchema); + } + + private string MapPathWithParameters(string path, IEnumerable? parameters) + { + if (parameters == null) + { + return path; + } + + var newPath = path; + foreach (var parameter in parameters) + { + var exampleMatcherModel = GetExampleMatcherModel(parameter.Schema, _settings.PathPatternToUse); + newPath = newPath.Replace($"{{{parameter.Name}}}", exampleMatcherModel.Pattern as string); + } + + return newPath; + } + + private IDictionary? MapHeaders(string? responseContentType, IDictionary? headers) + { + var mappedHeaders = headers? + .ToDictionary(item => item.Key, _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!) ?? new Dictionary(); + + if (!string.IsNullOrEmpty(responseContentType)) + { + mappedHeaders.TryAdd(HeaderContentType, responseContentType); + } + + return mappedHeaders.Keys.Any() ? mappedHeaders : null; + } + + private IList? MapQueryParameters(IEnumerable queryParameters) + { + var list = queryParameters + .Where(req => req.Required) + .Select(qp => new ParamModel + { + Name = qp.Name ?? string.Empty, + IgnoreCase = _settings.QueryParameterPatternIgnoreCase, + Matchers = + [ + GetExampleMatcherModel(qp.Schema, _settings.QueryParameterPatternToUse) + ] + }) + .ToList(); + + return list.Any() ? list : null; + } + + private IList? MapRequestHeaders(IEnumerable headers) + { + var list = headers + .Where(req => req.Required) + .Select(qp => new HeaderModel + { + Name = qp.Name ?? string.Empty, + IgnoreCase = _settings.HeaderPatternIgnoreCase, + Matchers = + [ + GetExampleMatcherModel(qp.Schema, _settings.HeaderPatternToUse) + ] + }) + .ToList(); + + return list.Any() ? list : null; + } + + private MatcherModel GetExampleMatcherModel(IOpenApiSchema? schema, ExampleValueType type) + { + return type switch + { + ExampleValueType.Value => new MatcherModel + { + Name = "ExactMatcher", + Pattern = GetExampleValueAsStringForSchemaType(schema), + IgnoreCase = _settings.IgnoreCaseExampleValues + }, + + _ => new MatcherModel + { + Name = "WildcardMatcher", + Pattern = "*" + } + }; + } + + private string GetExampleValueAsStringForSchemaType(IOpenApiSchema? schema) + { + var value = _exampleValueGenerator.GetExampleValue(schema); + + if (value.GetValueKind() == JsonValueKind.String) + { + return value.GetValue(); + } + + return value.ToString(); + } + + private static string MapBasePath(IList? 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; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Properties/AssemblyInfo.cs b/src/WireMock.Net.OpenApiParser.Preview/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..fb354ea5 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright © WireMock.Net + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Settings/IWireMockOpenApiParserExampleValues.cs b/src/WireMock.Net.OpenApiParser.Preview/Settings/IWireMockOpenApiParserExampleValues.cs new file mode 100644 index 00000000..5d335144 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Settings/IWireMockOpenApiParserExampleValues.cs @@ -0,0 +1,62 @@ +// Copyright © WireMock.Net + +using System; +using Microsoft.OpenApi.Models.Interfaces; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// An interface defining the example values to use for the different types. +/// +public interface IWireMockOpenApiParserExampleValues +{ + /// + /// An example value for a Boolean. + /// + bool Boolean { get; } + + /// + /// An example value for an Integer. + /// + int Integer { get; } + + /// + /// An example value for a Float. + /// + float Float { get; } + + /// + /// An example value for a Decimal. + /// + decimal Decimal { get; } + + /// + /// An example value for a Date. + /// + Func Date { get; } + + /// + /// An example value for a DateTime. + /// + Func DateTime { get; } + + /// + /// An example value for Bytes. + /// + byte[] Bytes { get; } + + /// + /// An example value for a Object. + /// + object Object { get; } + + /// + /// An example value for a String. + /// + string String { get; } + + /// + /// OpenApi Schema to generate dynamic examples more accurate + /// + IOpenApiSchema? Schema { get; set; } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserDynamicExampleValues.cs b/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserDynamicExampleValues.cs new file mode 100644 index 00000000..194762c5 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserDynamicExampleValues.cs @@ -0,0 +1,59 @@ +// Copyright © WireMock.Net + +using System; +using Microsoft.OpenApi.Models.Interfaces; +using RandomDataGenerator.FieldOptions; +using RandomDataGenerator.Randomizers; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// A class defining the random example values to use for the different types. +/// +public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserExampleValues +{ + /// + public virtual bool Boolean => RandomizerFactory.GetRandomizer(new FieldOptionsBoolean()).Generate() ?? true; + + /// + public virtual int Integer => RandomizerFactory.GetRandomizer(new FieldOptionsInteger()).Generate() ?? 42; + + /// + public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f; + + /// + public virtual decimal Decimal => SafeConvertFloatToDecimal(RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f); + + /// + public virtual Func Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date; + + /// + public virtual Func DateTime => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow; + + /// + public virtual byte[] Bytes => RandomizerFactory.GetRandomizer(new FieldOptionsBytes()).Generate(); + + /// + public virtual object Object => "example-object"; + + /// + public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string"; + + /// + public virtual IOpenApiSchema? Schema { get; set; } + + /// + /// Safely converts a float to a decimal, ensuring the value stays within the bounds of a decimal. + /// + /// The float value to convert. + /// A decimal value within the valid range of a decimal. + private static decimal SafeConvertFloatToDecimal(float value) + { + return value switch + { + < (float)decimal.MinValue => decimal.MinValue, + > (float)decimal.MaxValue => decimal.MaxValue, + _ => (decimal)value + }; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserExampleValues.cs b/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserExampleValues.cs new file mode 100644 index 00000000..3b3c6fea --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserExampleValues.cs @@ -0,0 +1,43 @@ +// Copyright © WireMock.Net + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// A class defining the example values to use for the different types. +/// +public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleValues +{ + /// + public virtual bool Boolean => true; + + /// + public virtual int Integer => 42; + + /// + public virtual float Float => 4.2f; + + /// + public virtual decimal Decimal => 4.2m; + + /// + public virtual Func Date { get; } = () => System.DateTime.UtcNow.Date; + + /// + public virtual Func DateTime { get; } = () => System.DateTime.UtcNow; + + /// + public virtual byte[] Bytes { get; } = [48, 49, 50]; + + /// + public virtual object Object => "example-object"; + + /// + public virtual string String => "example-string"; + + /// + public virtual IOpenApiSchema? Schema { get; set; } = new OpenApiSchema(); +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserSettings.cs b/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserSettings.cs new file mode 100644 index 00000000..ea3cc600 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Settings/WireMockOpenApiParserSettings.cs @@ -0,0 +1,73 @@ +// Copyright © WireMock.Net + +using WireMock.Net.OpenApiParser.Types; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// The WireMockOpenApiParser Settings +/// +public class WireMockOpenApiParserSettings +{ + /// + /// The number of array items to generate (default is 3). + /// + public int NumberOfArrayItems { get; set; } = 3; + + /// + /// The example value type to use when generating a Path + /// + public ExampleValueType PathPatternToUse { get; set; } = ExampleValueType.Value; + + /// + /// The example value type to use when generating a Header + /// + public ExampleValueType HeaderPatternToUse { get; set; } = ExampleValueType.Value; + + /// + /// The example value type to use when generating a Query Parameter + /// + public ExampleValueType QueryParameterPatternToUse { get; set; } = ExampleValueType.Value; + + /// + /// The example values to use. + /// + /// Default implementations are: + /// - + /// - + /// + public IWireMockOpenApiParserExampleValues? ExampleValues { get; set; } + + /// + /// Is a Header match case-insensitive? + /// + /// Default is true. + /// + public bool HeaderPatternIgnoreCase { get; set; } = true; + + /// + /// Is a Query Parameter match case-insensitive? + /// + /// Default is true. + /// + public bool QueryParameterPatternIgnoreCase { get; set; } = true; + + /// + /// Is a Request Body match case-insensitive? + /// + /// Default is true. + /// + public bool RequestBodyIgnoreCase { get; set; } = true; + + /// + /// Is a ExampleValue match case-insensitive? + /// + /// Default is true. + /// + public bool IgnoreCaseExampleValues { get; set; } = true; + + /// + /// Are examples generated dynamically? + /// + public bool DynamicExamples { get; set; } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Types/ExampleValueType.cs b/src/WireMock.Net.OpenApiParser.Preview/Types/ExampleValueType.cs new file mode 100644 index 00000000..98856b3a --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Types/ExampleValueType.cs @@ -0,0 +1,21 @@ +// Copyright © WireMock.Net + +namespace WireMock.Net.OpenApiParser.Types; + +/// +/// The example value to use +/// +public enum ExampleValueType +{ + /// + /// 1. Use a generated example value based on the SchemaType (default). + /// 2. If there is no example value defined in the schema, + /// then the will be used (custom, fixed or dynamic). + /// + Value, + + /// + /// Just use a Wildcard (*) character. + /// + Wildcard +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Types/SchemaFormat.cs b/src/WireMock.Net.OpenApiParser.Preview/Types/SchemaFormat.cs new file mode 100644 index 00000000..c5370238 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Types/SchemaFormat.cs @@ -0,0 +1,26 @@ +// Copyright © WireMock.Net + +namespace WireMock.Net.OpenApiParser.Types; + +internal enum SchemaFormat +{ + Float, + + Double, + + Int32, + + Int64, + + Date, + + DateTime, + + Password, + + Byte, + + Binary, + + Undefined +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Utils/DateTimeUtils.cs b/src/WireMock.Net.OpenApiParser.Preview/Utils/DateTimeUtils.cs new file mode 100644 index 00000000..e3178549 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Utils/DateTimeUtils.cs @@ -0,0 +1,22 @@ +// Copyright © WireMock.Net + +using System; +using System.Globalization; + +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(DateTimeFormat, DateTimeFormatInfo.InvariantInfo); + } + + public static string ToRfc3339Date(DateTime dateTime) + { + return dateTime.ToString(DateFormat, DateTimeFormatInfo.InvariantInfo); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Utils/ExampleValueGenerator.cs b/src/WireMock.Net.OpenApiParser.Preview/Utils/ExampleValueGenerator.cs new file mode 100644 index 00000000..153d4c67 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Utils/ExampleValueGenerator.cs @@ -0,0 +1,105 @@ +// Copyright © WireMock.Net + +using System; +using System.Linq; +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; +using WireMock.Net.OpenApiParser.Types; + +namespace WireMock.Net.OpenApiParser.Utils; + +internal class ExampleValueGenerator +{ + private readonly IWireMockOpenApiParserExampleValues _exampleValues; + + public ExampleValueGenerator(WireMockOpenApiParserSettings settings) + { + Guard.NotNull(settings); + + // Check if user provided an own implementation + if (settings.ExampleValues is null) + { + if (settings.DynamicExamples) + { + _exampleValues = new WireMockOpenApiParserDynamicExampleValues(); + } + else + { + _exampleValues = new WireMockOpenApiParserExampleValues(); + } + } + else + { + _exampleValues = settings.ExampleValues; + } + } + + public JsonNode GetExampleValue(IOpenApiSchema? schema) + { + var schemaExample = schema?.Example; + var schemaEnum = schema?.Enum?.FirstOrDefault(); + + _exampleValues.Schema = schema; + + switch (schema?.GetSchemaType(out _)) + { + case JsonSchemaType.Boolean: + var exampleBoolean = schemaExample?.GetValue(); + return exampleBoolean ?? _exampleValues.Boolean; + + case JsonSchemaType.Integer: + var exampleInteger = schemaExample?.GetValue(); + var enumInteger = schemaEnum?.GetValue(); + var valueIntegerEnumOrExample = enumInteger ?? exampleInteger; + return valueIntegerEnumOrExample ?? _exampleValues.Integer; + + case JsonSchemaType.Number: + switch (schema.GetSchemaFormat()) + { + case SchemaFormat.Float: + var exampleFloat = schemaExample?.GetValue(); + var enumFloat = schemaEnum?.GetValue(); + var valueFloatEnumOrExample = enumFloat ?? exampleFloat; + return valueFloatEnumOrExample ?? _exampleValues.Float; + + default: + var exampleDecimal = schemaExample?.GetValue(); + var enumDecimal = schemaEnum?.GetValue(); + var valueDecimalEnumOrExample = enumDecimal ?? exampleDecimal; + return valueDecimalEnumOrExample ?? _exampleValues.Decimal; + } + + default: + switch (schema?.GetSchemaFormat()) + { + case SchemaFormat.Date: + var exampleDate = schemaExample?.GetValue(); + var enumDate = schemaEnum?.GetValue(); + var valueDateEnumOrExample = enumDate ?? exampleDate; + return valueDateEnumOrExample ?? DateTimeUtils.ToRfc3339Date(_exampleValues.Date()); + + case SchemaFormat.DateTime: + var exampleDateTime = schemaExample?.GetValue(); + var enumDateTime = schemaEnum?.GetValue(); + var valueDateTimeEnumOrExample = enumDateTime ?? exampleDateTime; + return valueDateTimeEnumOrExample ?? DateTimeUtils.ToRfc3339DateTime(_exampleValues.DateTime()); + + case SchemaFormat.Byte: + var exampleByte = schemaExample?.GetValue(); + var enumByte = schemaEnum?.GetValue(); + var valueByteEnumOrExample = enumByte ?? exampleByte; + return Convert.ToBase64String(valueByteEnumOrExample ?? _exampleValues.Bytes); + + default: + var exampleString = schemaExample?.GetValue(); + var enumString = schemaEnum?.GetValue(); + var valueStringEnumOrExample = enumString ?? exampleString; + return valueStringEnumOrExample ?? _exampleValues.String; + } + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/Utils/PathUtils.cs b/src/WireMock.Net.OpenApiParser.Preview/Utils/PathUtils.cs new file mode 100644 index 00000000..c46b3923 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/Utils/PathUtils.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/WireMock.Net.OpenApiParser.Preview.csproj b/src/WireMock.Net.OpenApiParser.Preview/WireMock.Net.OpenApiParser.Preview.csproj new file mode 100644 index 00000000..41fdaecf --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/WireMock.Net.OpenApiParser.Preview.csproj @@ -0,0 +1,36 @@ + + + + An OpenApi (swagger) parser to generate MappingModel or mapping.json file. + net47;netstandard2.0;netstandard2.1;net8.0 + true + wiremock;openapi;OAS;raml;converter;parser;openapiparser + {E5B03EEF-822C-4295-952B-4479AD30082B} + true + ../WireMock.Net/WireMock.Net.ruleset + true + ../WireMock.Net/WireMock.Net.snk + true + MIT + + + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser.Preview/WireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser.Preview/WireMockOpenApiParser.cs new file mode 100644 index 00000000..849da722 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser.Preview/WireMockOpenApiParser.cs @@ -0,0 +1,107 @@ +// Copyright © WireMock.Net + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.YamlReader; +using RamlToOpenApiConverter; +using WireMock.Admin.Mappings; +using WireMock.Net.OpenApiParser.Mappers; +using WireMock.Net.OpenApiParser.Settings; + +namespace WireMock.Net.OpenApiParser; + +/// +/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock.Net MappingModels. +/// +public class WireMockOpenApiParser : IWireMockOpenApiParser +{ + private static readonly OpenApiReaderSettings ReaderSettings = new(); + + /// + [PublicAPI] + public IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic) + { + return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + [PublicAPI] + public IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + OpenApiDocument document; + if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase)) + { + diagnostic = new OpenApiDiagnostic(); + document = new RamlConverter().ConvertToOpenApiDocument(path); + } + else + { + document = Read(File.OpenRead(path), out diagnostic); + } + + return FromDocument(document, settings); + } + + /// + [PublicAPI] + public IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) + { + return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers ?? []); + } + + /// + [PublicAPI] + public IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic) + { + return FromDocument(Read(stream, out diagnostic)); + } + + /// + [PublicAPI] + public IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + return FromDocument(Read(stream, out diagnostic), settings); + } + + /// + [PublicAPI] + public IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic) + { + return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic); + } + + /// + [PublicAPI] + public IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + 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; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Extensions/DictionaryExtensions.cs b/src/WireMock.Net.OpenApiParser/Extensions/DictionaryExtensions.cs index a819a1f9..8034318e 100644 --- a/src/WireMock.Net.OpenApiParser/Extensions/DictionaryExtensions.cs +++ b/src/WireMock.Net.OpenApiParser/Extensions/DictionaryExtensions.cs @@ -1,6 +1,6 @@ // Copyright © WireMock.Net -#if NET46 || NET47 || NETSTANDARD2_0 +#if NET46 || NETSTANDARD2_0 using System.Collections.Generic; namespace WireMock.Net.OpenApiParser.Extensions; diff --git a/src/WireMock.Net.OpenApiParser/Extensions/OpenApiSchemaExtensions.cs b/src/WireMock.Net.OpenApiParser/Extensions/OpenApiSchemaExtensions.cs index 02f7a49b..efd4bfcd 100644 --- a/src/WireMock.Net.OpenApiParser/Extensions/OpenApiSchemaExtensions.cs +++ b/src/WireMock.Net.OpenApiParser/Extensions/OpenApiSchemaExtensions.cs @@ -1,53 +1,60 @@ // 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 { - public static bool TryGetXNullable(this IOpenApiSchema schema, out bool value) + /// + /// https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger + /// + public static bool TryGetXNullable(this OpenApiSchema schema, out bool value) { value = false; - if (schema.Extensions != null && schema.Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny { Node: { } jsonNode }) + if (schema.Extensions.TryGetValue("x-nullable", out var e) && e is OpenApiBoolean openApiBoolean) { - value = jsonNode.GetValueKind() == JsonValueKind.True; + value = openApiBoolean.Value; return true; } return false; } - public static JsonSchemaType? GetSchemaType(this IOpenApiSchema? schema, out bool isNullable) + public static SchemaType GetSchemaType(this OpenApiSchema? schema) { - isNullable = false; - if (schema == null) { - return null; + return SchemaType.Unknown; } if (schema.Type == null) { - if (schema.AllOf?.Any() == true || schema.AnyOf?.Any() == true) + if (schema.AllOf.Any() || schema.AnyOf.Any()) { - return JsonSchemaType.Object; + return SchemaType.Object; } } - 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; + 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 + }; } - public static SchemaFormat GetSchemaFormat(this IOpenApiSchema? schema) + public static SchemaFormat GetSchemaFormat(this OpenApiSchema? schema) { switch (schema?.Format) { diff --git a/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs b/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs index bd20f521..494d4f37 100644 --- a/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs +++ b/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs @@ -1,96 +1,96 @@ -// Copyright © WireMock.Net - -using System.IO; -using System.Linq; -using JetBrains.Annotations; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Reader; -using Stef.Validation; -using WireMock.Net.OpenApiParser.Settings; -using WireMock.Server; - -namespace WireMock.Net.OpenApiParser.Extensions; - -/// -/// Some extension methods for . -/// -public static class WireMockServerExtensions -{ - /// - /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file. - /// - /// The WireMockServer instance - /// Path containing OpenAPI file to parse and use the mappings. - /// Returns diagnostic object containing errors detected during parsing - [PublicAPI] - public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, out OpenApiDiagnostic diagnostic) - { - return WithMappingFromOpenApiFile(server, path, new WireMockOpenApiParserSettings(), out diagnostic); - } - - /// - /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file. - /// - /// The WireMockServer instance - /// Path containing OpenAPI file to parse and use the mappings. - /// Additional settings - /// Returns diagnostic object containing errors detected during parsing - [PublicAPI] - public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) - { - Guard.NotNull(server); - Guard.NotNullOrEmpty(path); - - var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic); - - return server.WithMapping(mappings.ToArray()); - } - - /// - /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream. - /// - /// The WireMockServer instance - /// Stream containing OpenAPI description to parse and use the mappings. - /// Returns diagnostic object containing errors detected during parsing - [PublicAPI] - public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, out OpenApiDiagnostic diagnostic) - { - return WithMappingFromOpenApiStream(server, stream, new WireMockOpenApiParserSettings(), out diagnostic); - } - - /// - /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream. - /// - /// The WireMockServer instance - /// Stream containing OpenAPI description to parse and use the mappings. - /// Additional settings - /// Returns diagnostic object containing errors detected during parsing - [PublicAPI] - public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) - { - Guard.NotNull(server); - Guard.NotNull(stream); - Guard.NotNull(settings); - - var mappings = new WireMockOpenApiParser().FromStream(stream, settings, out diagnostic); - - return server.WithMapping(mappings.ToArray()); - } - - /// - /// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 document. - /// - /// The WireMockServer instance - /// The OpenAPI document to use as mappings. - /// Additional settings [optional]. - [PublicAPI] - public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) - { - Guard.NotNull(server); - Guard.NotNull(document); - - var mappings = new WireMockOpenApiParser().FromDocument(document, settings); - - return server.WithMapping(mappings.ToArray()); - } +// Copyright © WireMock.Net + +using System.IO; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Stef.Validation; +using WireMock.Net.OpenApiParser.Settings; +using WireMock.Server; + +namespace WireMock.Net.OpenApiParser.Extensions; + +/// +/// Some extension methods for . +/// +public static class WireMockServerExtensions +{ + /// + /// Register the mappings via an OpenAPI (swagger) V2 or V3 file. + /// + /// The WireMockServer instance + /// Path containing OpenAPI file to parse and use the mappings. + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, out OpenApiDiagnostic diagnostic) + { + return WithMappingFromOpenApiFile(server, path, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2 or V3 file. + /// + /// The WireMockServer instance + /// Path containing OpenAPI file to parse and use the mappings. + /// Additional settings + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + Guard.NotNull(server); + Guard.NotNullOrEmpty(path); + + var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic); + + return server.WithMapping(mappings.ToArray()); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2 or V3 stream. + /// + /// The WireMockServer instance + /// Stream containing OpenAPI description to parse and use the mappings. + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, out OpenApiDiagnostic diagnostic) + { + return WithMappingFromOpenApiStream(server, stream, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2 or V3 stream. + /// + /// The WireMockServer instance + /// Stream containing OpenAPI description to parse and use the mappings. + /// Additional settings + /// Returns diagnostic object containing errors detected during parsing + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + Guard.NotNull(server); + Guard.NotNull(stream); + Guard.NotNull(settings); + + var mappings = new WireMockOpenApiParser().FromStream(stream, settings, out diagnostic); + + return server.WithMapping(mappings.ToArray()); + } + + /// + /// Register the mappings via an OpenAPI (swagger) V2 or V3 document. + /// + /// The WireMockServer instance + /// The OpenAPI document to use as mappings. + /// Additional settings [optional]. + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) + { + Guard.NotNull(server); + Guard.NotNull(document); + + var mappings = new WireMockOpenApiParser().FromDocument(document, settings); + + return server.WithMapping(mappings.ToArray()); + } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs index 4118414c..e3b2b728 100644 --- a/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs +++ b/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs @@ -1,75 +1,75 @@ -// Copyright © WireMock.Net - -using System.Collections.Generic; -using System.IO; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Reader; -using WireMock.Admin.Mappings; -using WireMock.Net.OpenApiParser.Settings; - -namespace WireMock.Net.OpenApiParser; - -/// -/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. -/// -public interface IWireMockOpenApiParser -{ - /// - /// Generate from a file-path. - /// - /// The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file. - /// OpenApiDiagnostic output - /// MappingModel - IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from a file-path. - /// - /// The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file. - /// Additional settings - /// OpenApiDiagnostic output - /// MappingModel - IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from an . - /// - /// The source OpenApiDocument - /// Additional settings [optional] - /// MappingModel - IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null); - - /// - /// Generate from a . - /// - /// The source stream - /// OpenApiDiagnostic output - /// MappingModel - IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from a . - /// - /// The source stream - /// Additional settings - /// OpenApiDiagnostic output - /// MappingModel - IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from a . - /// - /// The source text - /// OpenApiDiagnostic output - /// MappingModel - IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic); - - /// - /// Generate from a . - /// - /// The source text - /// Additional settings - /// OpenApiDiagnostic output - /// MappingModel - IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); +// Copyright © WireMock.Net + +using System.Collections.Generic; +using System.IO; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using WireMock.Admin.Mappings; +using WireMock.Net.OpenApiParser.Settings; + +namespace WireMock.Net.OpenApiParser; + +/// +/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. +/// +public interface IWireMockOpenApiParser +{ + /// + /// Generate from a file-path. + /// + /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a file-path. + /// + /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from an . + /// + /// The source OpenApiDocument + /// Additional settings [optional] + /// MappingModel + IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null); + + /// + /// Generate from a . + /// + /// The source stream + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source stream + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source text + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic); + + /// + /// Generate from a . + /// + /// The source text + /// Additional settings + /// OpenApiDiagnostic output + /// MappingModel + IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs b/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs index e48766e0..6056aecb 100644 --- a/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs +++ b/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs @@ -3,19 +3,20 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Models.Interfaces; +using Microsoft.OpenApi.Writers; 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; @@ -38,40 +39,56 @@ internal class OpenApiPathsMapper .OrderBy(p => p.Key) .Select(p => MapPath(p.Key, p.Value, servers)) .SelectMany(x => x) - .ToArray() ?? []; + .ToArray() ?? + Array.Empty(); } - private IReadOnlyList MapPath(string path, IOpenApiPathItem pathItem, IList servers) + private IReadOnlyList MapPaths(OpenApiPaths? paths, IList servers) { - return pathItem.Operations?.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray() ?? []; + return paths? + .OrderBy(p => p.Key) + .Select(p => MapPath(p.Key, p.Value, servers)) + .SelectMany(x => x) + .ToArray() ?? + Array.Empty(); + } + + private IReadOnlyList MapPath(string path, OpenApiPathItem pathItem, IList servers) + { + 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 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() ?? new KeyValuePair(); + var response = operation.Responses.FirstOrDefault(); - TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out var responseContentType); + TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out string? responseContentType); var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema; var responseExample = responseContent?.Example; var responseSchemaExample = responseContent?.Schema?.Example; - var responseBody = responseExample ?? responseSchemaExample ?? MapSchemaToObject(responseSchema); + var body = responseExample != null ? MapOpenApiAnyToJToken(responseExample) : + responseSchemaExample != null ? MapOpenApiAnyToJToken(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 var requestContent, out _); + TryGetContent(request, out OpenApiMediaType? requestContent, out _); var requestBodySchema = operation.RequestBody.Content.First().Value?.Schema; var requestBodyExample = requestContent!.Example; var requestBodySchemaExample = requestContent.Schema?.Example; - var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema); + var requestBodyMapped = requestBodyExample != null ? MapOpenApiAnyToJToken(requestBodyExample) : + requestBodySchemaExample != null ? MapOpenApiAnyToJToken(requestBodySchemaExample) : + MapSchemaToObject(requestBodySchema); + requestBodyModel = MapRequestBody(requestBodyMapped); } @@ -95,12 +112,12 @@ internal class OpenApiPathsMapper { StatusCode = httpStatusCode, Headers = MapHeaders(responseContentType, response.Value?.Headers), - BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null + BodyAsJson = body } }; } - private BodyModel? MapRequestBody(JsonNode? requestBody) + private BodyModel? MapRequestBody(object? requestBody) { if (requestBody == null) { @@ -112,7 +129,7 @@ internal class OpenApiPathsMapper Matcher = new MatcherModel { Name = "JsonMatcher", - Pattern = SystemTextJsonSerializer.Serialize(requestBody, new JsonSerializerOptions { WriteIndented = true }), + Pattern = JsonConvert.SerializeObject(requestBody, Formatting.Indented), IgnoreCase = _settings.RequestBodyIgnoreCase } }; @@ -143,103 +160,117 @@ internal class OpenApiPathsMapper return true; } - private JsonNode? MapSchemaToObject(IOpenApiSchema? schema) + private object? MapSchemaToObject(OpenApiSchema? schema, string? name = null) { if (schema == null) { return null; } - switch (schema.GetSchemaType(out _)) + switch (schema.GetSchemaType()) { - case JsonSchemaType.Array: - var array = new JsonArray(); - for (var i = 0; i < _settings.NumberOfArrayItems; i++) + case SchemaType.Array: + var jArray = new JArray(); + for (int i = 0; i < _settings.NumberOfArrayItems; i++) { - if (schema.Items?.Properties?.Count > 0) + if (schema.Items.Properties.Count > 0) { - var item = new JsonObject(); + var arrayItem = new JObject(); foreach (var property in schema.Items.Properties) { - item[property.Key] = MapSchemaToObject(property.Value); + var objectValue = MapSchemaToObject(property.Value, property.Key); + if (objectValue is JProperty jp) + { + arrayItem.Add(jp); + } + else + { + arrayItem.Add(new JProperty(property.Key, objectValue)); + } } - array.Add(item); + jArray.Add(arrayItem); } else { - var arrayItem = MapSchemaToObject(schema.Items); - array.Add(arrayItem); + var arrayItem = MapSchemaToObject(schema.Items, name: null); // Set name to null to force JObject instead of JProperty + jArray.Add(arrayItem); } } - if (schema.AllOf?.Count > 0) + if (schema.AllOf.Count > 0) { - array.Add(MapSchemaAllOfToObject(schema)); + jArray.Add(MapSchemaAllOfToObject(schema)); } - return array; + return jArray; - case JsonSchemaType.Boolean: - case JsonSchemaType.Integer: - case JsonSchemaType.Number: - case JsonSchemaType.String: + case SchemaType.Boolean: + case SchemaType.Integer: + case SchemaType.Number: + case SchemaType.String: return _exampleValueGenerator.GetExampleValue(schema); - case JsonSchemaType.Object: - var propertyAsJsonObject = new JsonObject(); - foreach (var schemaProperty in schema.Properties ?? new Dictionary()) + case SchemaType.Object: + var propertyAsJObject = new JObject(); + foreach (var schemaProperty in schema.Properties) { - propertyAsJsonObject[schemaProperty.Key] = MapPropertyAsJsonNode(schemaProperty.Value); + propertyAsJObject.Add(MapPropertyAsJObject(schemaProperty.Value, schemaProperty.Key)); } - if (schema.AllOf?.Count > 0) + if (schema.AllOf.Count > 0) { - foreach (var group in schema.AllOf.SelectMany(p => p.Properties ?? new Dictionary()).GroupBy(x => x.Key)) + foreach (var group in schema.AllOf.SelectMany(p => p.Properties).GroupBy(x => x.Key)) { - propertyAsJsonObject[group.Key] = MapPropertyAsJsonNode(group.First().Value); + propertyAsJObject.Add(MapPropertyAsJObject(group.First().Value, group.Key)); } } - return propertyAsJsonObject; + return name != null ? new JProperty(name, propertyAsJObject) : propertyAsJObject; default: return null; } } - private JsonObject MapSchemaAllOfToObject(IOpenApiSchema schema) + private JObject MapSchemaAllOfToObject(OpenApiSchema schema) { - var arrayItem = new JsonObject(); - foreach (var property in schema.AllOf ?? []) + var arrayItem = new JObject(); + foreach (var property in schema.AllOf) { - foreach (var item in property.Properties ?? new Dictionary()) + foreach (var item in property.Properties) { - arrayItem[item.Key] = MapPropertyAsJsonNode(item.Value); + arrayItem.Add(MapPropertyAsJObject(item.Value, item.Key)); } } return arrayItem; } - private JsonNode? MapPropertyAsJsonNode(IOpenApiSchema openApiSchema) + private object MapPropertyAsJObject(OpenApiSchema openApiSchema, string key) { - var schemaType = openApiSchema.GetSchemaType(out _); - if (schemaType is JsonSchemaType.Object or JsonSchemaType.Array) + if (openApiSchema.GetSchemaType() == SchemaType.Object || openApiSchema.GetSchemaType() == SchemaType.Array) { - return MapSchemaToObject(openApiSchema); + var mapped = MapSchemaToObject(openApiSchema, key); + if (mapped is JProperty jp) + { + return jp; + } + + return new JProperty(key, mapped); } - return _exampleValueGenerator.GetExampleValue(openApiSchema); + // bool propertyIsNullable = openApiSchema.Nullable || (openApiSchema.TryGetXNullable(out bool x) && x); + return new JProperty(key, _exampleValueGenerator.GetExampleValue(openApiSchema)); } - private string MapPathWithParameters(string path, IEnumerable? parameters) + private string MapPathWithParameters(string path, IEnumerable? parameters) { if (parameters == null) { return path; } - var newPath = path; + string newPath = path; foreach (var parameter in parameters) { var exampleMatcherModel = GetExampleMatcherModel(parameter.Schema, _settings.PathPatternToUse); @@ -249,56 +280,93 @@ internal class OpenApiPathsMapper return newPath; } - private IDictionary? MapHeaders(string? responseContentType, IDictionary? headers) + private string MapBasePath(IList? servers) { - var mappedHeaders = headers? - .ToDictionary(item => item.Key, _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!) ?? new Dictionary(); + 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? MapHeaders(string? responseContentType, IDictionary? headers) + { + var mappedHeaders = headers?.ToDictionary( + item => item.Key, + _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern! + ) ?? new Dictionary(); if (!string.IsNullOrEmpty(responseContentType)) { - mappedHeaders.TryAdd(HeaderContentType, responseContentType); + mappedHeaders.TryAdd(HeaderContentType, responseContentType!); } return mappedHeaders.Keys.Any() ? mappedHeaders : null; } - private IList? MapQueryParameters(IEnumerable queryParameters) + private IList? MapQueryParameters(IEnumerable queryParameters) { var list = queryParameters .Where(req => req.Required) .Select(qp => new ParamModel { - Name = qp.Name ?? string.Empty, + Name = qp.Name, IgnoreCase = _settings.QueryParameterPatternIgnoreCase, - Matchers = - [ + Matchers = new[] + { GetExampleMatcherModel(qp.Schema, _settings.QueryParameterPatternToUse) - ] + } }) .ToList(); return list.Any() ? list : null; } - private IList? MapRequestHeaders(IEnumerable headers) + private IList? MapRequestHeaders(IEnumerable headers) { var list = headers .Where(req => req.Required) .Select(qp => new HeaderModel { - Name = qp.Name ?? string.Empty, + Name = qp.Name, IgnoreCase = _settings.HeaderPatternIgnoreCase, - Matchers = - [ + Matchers = new[] + { GetExampleMatcherModel(qp.Schema, _settings.HeaderPatternToUse) - ] + } }) .ToList(); return list.Any() ? list : null; } - private MatcherModel GetExampleMatcherModel(IOpenApiSchema? schema, ExampleValueType type) + private MatcherModel GetExampleMatcherModel(OpenApiSchema? schema, ExampleValueType type) { return type switch { @@ -317,31 +385,15 @@ internal class OpenApiPathsMapper }; } - private string GetExampleValueAsStringForSchemaType(IOpenApiSchema? schema) + private string GetExampleValueAsStringForSchemaType(OpenApiSchema? schema) { var value = _exampleValueGenerator.GetExampleValue(schema); - if (value.GetValueKind() == JsonValueKind.String) + return value switch { - return value.GetValue(); - } + string valueAsString => valueAsString, - return value.ToString(); - } - - private static string MapBasePath(IList? 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; + _ => value.ToString(), + }; } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Settings/IWireMockOpenApiParserExampleValues.cs b/src/WireMock.Net.OpenApiParser/Settings/IWireMockOpenApiParserExampleValues.cs index 7c52339d..eecfed7e 100644 --- a/src/WireMock.Net.OpenApiParser/Settings/IWireMockOpenApiParserExampleValues.cs +++ b/src/WireMock.Net.OpenApiParser/Settings/IWireMockOpenApiParserExampleValues.cs @@ -1,62 +1,62 @@ -// Copyright © WireMock.Net - -using System; -using Microsoft.OpenApi.Models.Interfaces; - -namespace WireMock.Net.OpenApiParser.Settings; - -/// -/// A interface defining the example values to use for the different types. -/// -public interface IWireMockOpenApiParserExampleValues -{ - /// - /// An example value for a Boolean. - /// - bool Boolean { get; } - - /// - /// An example value for an Integer. - /// - int Integer { get; } - - /// - /// An example value for a Float. - /// - float Float { get; } - - /// - /// An example value for a Decimal. - /// - decimal Decimal { get; } - - /// - /// An example value for a Date. - /// - Func Date { get; } - - /// - /// An example value for a DateTime. - /// - Func DateTime { get; } - - /// - /// An example value for Bytes. - /// - byte[] Bytes { get; } - - /// - /// An example value for a Object. - /// - object Object { get; } - - /// - /// An example value for a String. - /// - string String { get; } - - /// - /// OpenApi Schema to generate dynamic examples more accurate - /// - IOpenApiSchema? Schema { get; set; } +// Copyright © WireMock.Net + +using System; +using Microsoft.OpenApi.Models; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// A interface defining the example values to use for the different types. +/// +public interface IWireMockOpenApiParserExampleValues +{ + /// + /// An example value for a Boolean. + /// + bool Boolean { get; } + + /// + /// An example value for an Integer. + /// + int Integer { get; } + + /// + /// An example value for a Float. + /// + float Float { get; } + + /// + /// An example value for a Double. + /// + double Double { get; } + + /// + /// An example value for a Date. + /// + Func Date { get; } + + /// + /// An example value for a DateTime. + /// + Func DateTime { get; } + + /// + /// An example value for Bytes. + /// + byte[] Bytes { get; } + + /// + /// An example value for a Object. + /// + object Object { get; } + + /// + /// An example value for a String. + /// + string String { get; } + + /// + /// OpenApi Schema to generate dynamic examples more accurate + /// + OpenApiSchema? Schema { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserDynamicExampleValues.cs b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserDynamicExampleValues.cs index c28e7016..46b2c766 100644 --- a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserDynamicExampleValues.cs +++ b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserDynamicExampleValues.cs @@ -1,59 +1,44 @@ -// Copyright © WireMock.Net - -using System; -using Microsoft.OpenApi.Models.Interfaces; -using RandomDataGenerator.FieldOptions; -using RandomDataGenerator.Randomizers; - -namespace WireMock.Net.OpenApiParser.Settings; - -/// -/// A class defining the random example values to use for the different types. -/// -public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserExampleValues -{ - /// - public virtual bool Boolean => RandomizerFactory.GetRandomizer(new FieldOptionsBoolean()).Generate() ?? true; - - /// - public virtual int Integer => RandomizerFactory.GetRandomizer(new FieldOptionsInteger()).Generate() ?? 42; - - /// - public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f; - - /// - public virtual decimal Decimal => SafeConvertFloatToDecimal(RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f); - - /// - public virtual Func Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date; - - /// - public virtual Func DateTime => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow; - - /// - public virtual byte[] Bytes => RandomizerFactory.GetRandomizer(new FieldOptionsBytes()).Generate(); - - /// - public virtual object Object => "example-object"; - - /// - public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string"; - - /// - public virtual IOpenApiSchema? Schema { get; set; } - - /// - /// Safely converts a float to a decimal, ensuring the value stays within the bounds of a decimal. - /// - /// The float value to convert. - /// A decimal value within the valid range of a decimal. - private static decimal SafeConvertFloatToDecimal(float value) - { - return value switch - { - < (float)decimal.MinValue => decimal.MinValue, - > (float)decimal.MaxValue => decimal.MaxValue, - _ => (decimal)value - }; - } +// Copyright © WireMock.Net + +using System; +using Microsoft.OpenApi.Models; +using RandomDataGenerator.FieldOptions; +using RandomDataGenerator.Randomizers; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// A class defining the random example values to use for the different types. +/// +public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserExampleValues +{ + /// + public virtual bool Boolean => RandomizerFactory.GetRandomizer(new FieldOptionsBoolean()).Generate() ?? true; + + /// + public virtual int Integer => RandomizerFactory.GetRandomizer(new FieldOptionsInteger()).Generate() ?? 42; + + /// + public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f; + + /// + public virtual double Double => RandomizerFactory.GetRandomizer(new FieldOptionsDouble()).Generate() ?? 4.2d; + + /// + public virtual Func Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date; + + /// + public virtual Func DateTime => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow; + + /// + public virtual byte[] Bytes => RandomizerFactory.GetRandomizer(new FieldOptionsBytes()).Generate(); + + /// + public virtual object Object => "example-object"; + + /// + public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string"; + + /// + public virtual OpenApiSchema? Schema { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs index 4a43b424..99665cf0 100644 --- a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs +++ b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs @@ -1,43 +1,42 @@ -// Copyright © WireMock.Net - -using System; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Models.Interfaces; - -namespace WireMock.Net.OpenApiParser.Settings; - -/// -/// A class defining the example values to use for the different types. -/// -public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleValues -{ - /// - public virtual bool Boolean => true; - - /// - public virtual int Integer => 42; - - /// - public virtual float Float => 4.2f; - - /// - public virtual decimal Decimal => 4.2m; - - /// - public virtual Func Date { get; } = () => System.DateTime.UtcNow.Date; - - /// - public virtual Func DateTime { get; } = () => System.DateTime.UtcNow; - - /// - public virtual byte[] Bytes { get; } = [48, 49, 50]; - - /// - public virtual object Object => "example-object"; - - /// - public virtual string String => "example-string"; - - /// - public virtual IOpenApiSchema? Schema { get; set; } = new OpenApiSchema(); +// Copyright © WireMock.Net + +using System; +using Microsoft.OpenApi.Models; + +namespace WireMock.Net.OpenApiParser.Settings; + +/// +/// A class defining the example values to use for the different types. +/// +public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleValues +{ + /// + public virtual bool Boolean => true; + + /// + public virtual int Integer => 42; + + /// + public virtual float Float => 4.2f; + + /// + public virtual double Double => 4.2d; + + /// + public virtual Func Date { get; } = () => System.DateTime.UtcNow.Date; + + /// + public virtual Func DateTime { get; } = () => System.DateTime.UtcNow; + + /// + public virtual byte[] Bytes { get; } = { 48, 49, 50 }; + + /// + public virtual object Object => "example-object"; + + /// + public virtual string String => "example-string"; + + /// + public virtual OpenApiSchema? Schema { get; set; } = new(); } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Types/SchemaType.cs b/src/WireMock.Net.OpenApiParser/Types/SchemaType.cs new file mode 100644 index 00000000..4b5df64d --- /dev/null +++ b/src/WireMock.Net.OpenApiParser/Types/SchemaType.cs @@ -0,0 +1,22 @@ +// Copyright © WireMock.Net + +namespace WireMock.Net.OpenApiParser.Types; + +internal enum SchemaType +{ + Object, + + Array, + + String, + + Integer, + + Number, + + Boolean, + + File, + + Unknown +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Utils/DateTimeUtils.cs b/src/WireMock.Net.OpenApiParser/Utils/DateTimeUtils.cs index e3178549..c0108295 100644 --- a/src/WireMock.Net.OpenApiParser/Utils/DateTimeUtils.cs +++ b/src/WireMock.Net.OpenApiParser/Utils/DateTimeUtils.cs @@ -7,16 +7,13 @@ 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(DateTimeFormat, DateTimeFormatInfo.InvariantInfo); + return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo); } public static string ToRfc3339Date(DateTime dateTime) { - return dateTime.ToString(DateFormat, DateTimeFormatInfo.InvariantInfo); + return dateTime.ToString("yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo); } } \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs b/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs index 153d4c67..4039a358 100644 --- a/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs +++ b/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs @@ -1,10 +1,8 @@ // Copyright © WireMock.Net -using System; using System.Linq; -using System.Text.Json.Nodes; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Models.Interfaces; using Stef.Validation; using WireMock.Net.OpenApiParser.Extensions; using WireMock.Net.OpenApiParser.Settings; @@ -38,66 +36,82 @@ internal class ExampleValueGenerator } } - public JsonNode GetExampleValue(IOpenApiSchema? schema) + public object GetExampleValue(OpenApiSchema? schema) { var schemaExample = schema?.Example; var schemaEnum = schema?.Enum?.FirstOrDefault(); _exampleValues.Schema = schema; - switch (schema?.GetSchemaType(out _)) + switch (schema?.GetSchemaType()) { - case JsonSchemaType.Boolean: - var exampleBoolean = schemaExample?.GetValue(); - return exampleBoolean ?? _exampleValues.Boolean; + case SchemaType.Boolean: + var exampleBoolean = schemaExample as OpenApiBoolean; + return exampleBoolean?.Value ?? _exampleValues.Boolean; - case JsonSchemaType.Integer: - var exampleInteger = schemaExample?.GetValue(); - var enumInteger = schemaEnum?.GetValue(); - var valueIntegerEnumOrExample = enumInteger ?? exampleInteger; - return valueIntegerEnumOrExample ?? _exampleValues.Integer; + 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.Number: - switch (schema.GetSchemaFormat()) + 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 SchemaFormat.Float: - var exampleFloat = schemaExample?.GetValue(); - var enumFloat = schemaEnum?.GetValue(); - var valueFloatEnumOrExample = enumFloat ?? exampleFloat; + var exampleFloat = schemaExample as OpenApiFloat; + var enumFloat = schemaEnum as OpenApiFloat; + var valueFloatEnumOrExample = enumFloat?.Value ?? exampleFloat?.Value; return valueFloatEnumOrExample ?? _exampleValues.Float; default: - var exampleDecimal = schemaExample?.GetValue(); - var enumDecimal = schemaEnum?.GetValue(); - var valueDecimalEnumOrExample = enumDecimal ?? exampleDecimal; - return valueDecimalEnumOrExample ?? _exampleValues.Decimal; + var exampleDouble = schemaExample as OpenApiDouble; + var enumDouble = schemaEnum as OpenApiDouble; + var valueDoubleEnumOrExample = enumDouble?.Value ?? exampleDouble?.Value; + return valueDoubleEnumOrExample ?? _exampleValues.Double; } default: switch (schema?.GetSchemaFormat()) { case SchemaFormat.Date: - var exampleDate = schemaExample?.GetValue(); - var enumDate = schemaEnum?.GetValue(); - var valueDateEnumOrExample = enumDate ?? exampleDate; - return valueDateEnumOrExample ?? DateTimeUtils.ToRfc3339Date(_exampleValues.Date()); + var exampleDate = schemaExample as OpenApiDate; + var enumDate = schemaEnum as OpenApiDate; + var valueDateEnumOrExample = enumDate?.Value ?? exampleDate?.Value; + return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _exampleValues.Date()); case SchemaFormat.DateTime: - var exampleDateTime = schemaExample?.GetValue(); - var enumDateTime = schemaEnum?.GetValue(); - var valueDateTimeEnumOrExample = enumDateTime ?? exampleDateTime; - return valueDateTimeEnumOrExample ?? DateTimeUtils.ToRfc3339DateTime(_exampleValues.DateTime()); + var exampleDateTime = schemaExample as OpenApiDateTime; + var enumDateTime = schemaEnum as OpenApiDateTime; + var valueDateTimeEnumOrExample = enumDateTime?.Value ?? exampleDateTime?.Value; + return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _exampleValues.DateTime()); case SchemaFormat.Byte: - var exampleByte = schemaExample?.GetValue(); - var enumByte = schemaEnum?.GetValue(); - var valueByteEnumOrExample = enumByte ?? exampleByte; - return Convert.ToBase64String(valueByteEnumOrExample ?? _exampleValues.Bytes); + 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; default: - var exampleString = schemaExample?.GetValue(); - var enumString = schemaEnum?.GetValue(); - var valueStringEnumOrExample = enumString ?? exampleString; + var exampleString = schemaExample as OpenApiString; + var enumString = schemaEnum as OpenApiString; + var valueStringEnumOrExample = enumString?.Value ?? exampleString?.Value; return valueStringEnumOrExample ?? _exampleValues.String; } } diff --git a/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj b/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj index d6053b02..9fac3c76 100644 --- a/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj +++ b/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj @@ -1,36 +1,37 @@ - - - - An OpenApi (swagger) parser to generate MappingModel or mapping.json file. - net47;netstandard2.0;netstandard2.1;net8.0 - true - wiremock;openapi;OAS;raml;converter;parser;openapiparser - {D3804228-91F4-4502-9595-39584E5AADAD} - true - ../WireMock.Net/WireMock.Net.ruleset - true - ../WireMock.Net/WireMock.Net.snk - true - MIT - - - - true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - + + + + An OpenApi (swagger) parser to generate MappingModel or mapping.json file. + net46;netstandard2.0;netstandard2.1 + true + wiremock;openapi;OAS;raml;converter;parser;openapiparser + {D3804228-91F4-4502-9595-39584E5AADAD} + true + ../WireMock.Net/WireMock.Net.ruleset + true + ../WireMock.Net/WireMock.Net.snk + true + MIT + + + + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs index 9db653d8..c027eed9 100644 --- a/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs +++ b/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs @@ -1,107 +1,84 @@ -// Copyright © WireMock.Net - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using JetBrains.Annotations; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Reader; -using Microsoft.OpenApi.YamlReader; -using RamlToOpenApiConverter; -using WireMock.Admin.Mappings; -using WireMock.Net.OpenApiParser.Mappers; -using WireMock.Net.OpenApiParser.Settings; - -namespace WireMock.Net.OpenApiParser; - -/// -/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock.Net MappingModels. -/// -public class WireMockOpenApiParser : IWireMockOpenApiParser -{ - private static readonly OpenApiReaderSettings ReaderSettings = new(); - - /// - [PublicAPI] - public IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic) - { - return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic); - } - - /// - [PublicAPI] - public IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) - { - OpenApiDocument document; - if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase)) - { - diagnostic = new OpenApiDiagnostic(); - document = new RamlConverter().ConvertToOpenApiDocument(path); - } - else - { - document = Read(File.OpenRead(path), out diagnostic); - } - - return FromDocument(document, settings); - } - - /// - [PublicAPI] - public IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) - { - return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers ?? []); - } - - /// - [PublicAPI] - public IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic) - { - return FromDocument(Read(stream, out diagnostic)); - } - - /// - [PublicAPI] - public IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) - { - return FromDocument(Read(stream, out diagnostic), settings); - } - - /// - [PublicAPI] - public IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic) - { - return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic); - } - - /// - [PublicAPI] - public IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) - { - 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; - } +// Copyright © WireMock.Net + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using RamlToOpenApiConverter; +using WireMock.Admin.Mappings; +using WireMock.Net.OpenApiParser.Mappers; +using WireMock.Net.OpenApiParser.Settings; + +namespace WireMock.Net.OpenApiParser; + +/// +/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock.Net MappingModels. +/// +public class WireMockOpenApiParser : IWireMockOpenApiParser +{ + private readonly OpenApiStreamReader _reader = new(); + + /// + [PublicAPI] + public IReadOnlyList FromFile(string path, out OpenApiDiagnostic diagnostic) + { + return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + [PublicAPI] + public IReadOnlyList FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + OpenApiDocument document; + if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase)) + { + diagnostic = new OpenApiDiagnostic(); + document = new RamlConverter().ConvertToOpenApiDocument(path); + } + else + { + var reader = new OpenApiStreamReader(); + document = reader.Read(File.OpenRead(path), out diagnostic); + } + + return FromDocument(document, settings); + } + + /// + [PublicAPI] + public IReadOnlyList FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null) + { + return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers); + } + + /// + [PublicAPI] + public IReadOnlyList FromStream(Stream stream, out OpenApiDiagnostic diagnostic) + { + return FromDocument(_reader.Read(stream, out diagnostic)); + } + + /// + [PublicAPI] + public IReadOnlyList FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + return FromDocument(_reader.Read(stream, out diagnostic), settings); + } + + /// + [PublicAPI] + public IReadOnlyList FromText(string text, out OpenApiDiagnostic diagnostic) + { + return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic); + } + + /// + [PublicAPI] + public IReadOnlyList FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic); + } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/TypeLoader.cs b/src/WireMock.Net/Util/TypeLoader.cs index 23e8e633..2be7e020 100644 --- a/src/WireMock.Net/Util/TypeLoader.cs +++ b/src/WireMock.Net/Util/TypeLoader.cs @@ -25,7 +25,7 @@ internal static class TypeLoader return foundType; } - throw new DllNotFoundException($"No dll found which implements Interface '{key}'."); + throw new DllNotFoundException($"No dll found which implements interface '{key}'."); }); return (TInterface)Activator.CreateInstance(pluginType, args)!; diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 168787c3..0edb2b2f 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -46,7 +46,7 @@ $(DefineConstants);USE_ASPNETCORE;NET46 - + $(DefineConstants);OPENAPIPARSER @@ -205,7 +205,7 @@ - + \ No newline at end of file diff --git a/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.FromText_ShouldReturnMappings.verified.txt b/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.FromText_ShouldReturnMappings.verified.txt index 382d8455..7190423d 100644 --- a/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.FromText_ShouldReturnMappings.verified.txt +++ b/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.FromText_ShouldReturnMappings.verified.txt @@ -334,7 +334,7 @@ "processingTerminalId": "example-string", "order": { "orderId": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00", + "dateTime": "2024-06-19T12:34:56.000+00:00", "description": "example-string", "amount": 42, "currency": "AED", @@ -1787,7 +1787,7 @@ "processingTerminalId": "example-string", "order": { "orderId": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00", + "dateTime": "2024-06-19T12:34:56.000+00:00", "description": "example-string", "amount": 42, "currency": "AED" @@ -2714,7 +2714,7 @@ "processingTerminalId": "example-string", "order": { "orderId": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00", + "dateTime": "2024-06-19T12:34:56.000+00:00", "description": "example-string", "amount": 42, "currency": "AED", @@ -2863,7 +2863,7 @@ "processingTerminalId": "example-string", "order": { "orderId": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00", + "dateTime": "2024-06-19T12:34:56.000+00:00", "description": "example-string", "amount": 42, "currency": "AED" @@ -6893,7 +6893,7 @@ "operator": "example-string", "order": { "orderId": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00", + "dateTime": "2024-06-19T12:34:56.000+00:00", "description": "example-string", "amount": 42, "currency": "AED", @@ -7093,7 +7093,7 @@ "offlineProcessing": { "operation": "offlineDecline", "approvalCode": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00" + "dateTime": "2024-06-19T12:34:56.000+00:00" }, "autoCapture": true, "processAsSale": true @@ -11349,7 +11349,7 @@ { "op": "replace", "path": "/a/b/c", - "value": 420 + "value": "420" }, { "op": "move", @@ -11827,7 +11827,7 @@ { "op": "replace", "path": "/a/b/c", - "value": 420 + "value": "420" }, { "op": "move", @@ -12541,7 +12541,7 @@ { "op": "replace", "path": "/a/b/c", - "value": 420 + "value": "420" }, { "op": "move", @@ -12986,7 +12986,7 @@ "operator": "example-string", "order": { "orderId": "example-string", - "dateTime": "2024-06-19T12:34:56.000\u002B00:00", + "dateTime": "2024-06-19T12:34:56.000+00:00", "description": "example-string", "amount": 42, "currency": "AED", diff --git a/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.cs b/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.cs index 2afddcaa..59da2970 100644 --- a/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.cs +++ b/test/WireMock.Net.Tests/OpenApiParser/WireMockOpenApiParserTests.cs @@ -23,7 +23,7 @@ public class WireMockOpenApiParserTests _exampleValuesMock.SetupGet(e => e.Boolean).Returns(true); _exampleValuesMock.SetupGet(e => e.Integer).Returns(42); _exampleValuesMock.SetupGet(e => e.Float).Returns(1.1f); - _exampleValuesMock.SetupGet(e => e.Decimal).Returns(2.2m); + _exampleValuesMock.SetupGet(e => e.Double).Returns(2.2); _exampleValuesMock.SetupGet(e => e.String).Returns("example-string"); _exampleValuesMock.SetupGet(e => e.Object).Returns("example-object"); _exampleValuesMock.SetupGet(e => e.Bytes).Returns("Stef"u8.ToArray());