From d91b5d583151cf4aa50458085263f038e65b1ddb Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 26 Apr 2021 12:44:45 +0000 Subject: [PATCH] Add possibility to use settings to generate MappingModel models with wildcard path parameters. (#609) * Add optional settings for WithMappingFromOpenApi * . * . * cleanup --- WireMock.Net Solution.sln.DotSettings | 1 + .../Run.cs | 13 +- .../Extensions/WireMockServerExtensions.cs | 66 ++++- .../IWireMockOpenApiParser.cs | 68 +++-- .../Mappers/OpenApiPathsMapper.cs | 276 ++++++++++++++++++ .../WireMockOpenApiParserExampleValues.cs | 30 ++ .../Settings/WireMockOpenApiParserSettings.cs | 30 ++ .../Types/ExampleValueType.cs | 18 ++ .../Utils/ExampleValueGenerator.cs | 30 +- .../WireMock.Net.OpenApiParser.csproj | 6 +- .../WireMockOpenApiParser.cs | 261 ++--------------- 11 files changed, 502 insertions(+), 297 deletions(-) create mode 100644 src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs create mode 100644 src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs create mode 100644 src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserSettings.cs create mode 100644 src/WireMock.Net.OpenApiParser/Types/ExampleValueType.cs diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings index f55a13e7c..79eca48e7 100644 --- a/WireMock.Net Solution.sln.DotSettings +++ b/WireMock.Net Solution.sln.DotSettings @@ -13,6 +13,7 @@ True True True + True True True \ No newline at end of file diff --git a/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs b/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs index 84a9ab4b0..92cc0cf89 100644 --- a/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs +++ b/examples/WireMock.Net.OpenApiParser.ConsoleApp/Run.cs @@ -3,9 +3,11 @@ using System.Linq; using WireMock.Admin.Mappings; using WireMock.Logging; +using WireMock.Net.OpenApiParser.Extensions; +using WireMock.Net.OpenApiParser.Settings; +using WireMock.Net.OpenApiParser.Types; using WireMock.Server; using WireMock.Settings; -using WireMock.Net.OpenApiParser.Extensions; namespace WireMock.Net.OpenApiParser.ConsoleApp { @@ -23,13 +25,18 @@ public static void RunServer(string path) ReadStaticMappings = false, WatchStaticMappings = false, WatchStaticMappingsInSubdirectories = false, - Logger = new WireMockConsoleLogger(), + Logger = new WireMockConsoleLogger() }); Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); server.SetBasicAuthentication("a", "b"); - server.WithMappingFromOpenApiFile(path, out var diag); + var settings = new WireMockOpenApiParserSettings + { + PathPatternToUse = ExampleValueType.Wildcard + }; + + server.WithMappingFromOpenApiFile(path, settings, out var diag); Console.WriteLine("Press any key to stop the server"); System.Console.ReadKey(); diff --git a/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs b/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs index eb3eedd0b..d195c36c0 100644 --- a/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs +++ b/src/WireMock.Net.OpenApiParser/Extensions/WireMockServerExtensions.cs @@ -1,12 +1,19 @@ -using System; -using System.IO; +using System.IO; using System.Linq; +using System.Runtime.InteropServices.ComTypes; +using JetBrains.Annotations; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; +using SharpYaml.Model; +using Stef.Validation; +using WireMock.Net.OpenApiParser.Settings; using WireMock.Server; namespace WireMock.Net.OpenApiParser.Extensions { + /// + /// Some extension methods for . + /// public static class WireMockServerExtensions { /// @@ -15,18 +22,26 @@ public static class WireMockServerExtensions /// 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) { - if (server == null) - { - throw new ArgumentNullException(nameof(server)); - } - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + return WithMappingFromOpenApiFile(server, path, null, 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. + /// Returns diagnostic object containing errors detected during parsing + /// Additional settings + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) + { + Guard.NotNull(server, nameof(server)); + Guard.NotNullOrEmpty(path, nameof(path)); - var mappings = new WireMockOpenApiParser().FromFile(path, out diagnostic); + var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic); return server.WithMapping(mappings.ToArray()); } @@ -37,9 +52,27 @@ public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer se /// 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) { - var mappings = new WireMockOpenApiParser().FromStream(stream, out diagnostic); + return WithMappingFromOpenApiStream(server, stream, null, 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, nameof(server)); + Guard.NotNull(stream, nameof(stream)); + Guard.NotNull(settings, nameof(settings)); + + var mappings = new WireMockOpenApiParser().FromStream(stream, settings, out diagnostic); return server.WithMapping(mappings.ToArray()); } @@ -49,9 +82,14 @@ public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer /// /// The WireMockServer instance /// The OpenAPI document to use as mappings. - public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document) + /// Additional settings [optional] + [PublicAPI] + public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings settings = null) { - var mappings = new WireMockOpenApiParser().FromDocument(document); + Guard.NotNull(server, nameof(server)); + Guard.NotNull(document, nameof(document)); + + var mappings = new WireMockOpenApiParser().FromDocument(document, settings); return server.WithMapping(mappings.ToArray()); } diff --git a/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs index 9cc3cb408..660f890de 100644 --- a/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs +++ b/src/WireMock.Net.OpenApiParser/IWireMockOpenApiParser.cs @@ -1,37 +1,57 @@ -using System.Collections.Generic; -using System.IO; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Readers; -using WireMock.Admin.Mappings; - -namespace WireMock.Net.OpenApiParser -{ - /// - /// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. - /// - public interface IWireMockOpenApiParser - { +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 + IEnumerable 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 + IEnumerable FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic); + /// /// Generate from an . /// /// The source OpenApiDocument - /// MappingModel - IEnumerable FromDocument(OpenApiDocument document); - + /// Additional settings [optional] + /// MappingModel + IEnumerable FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings settings = null); + /// /// Generate from a . /// /// The source stream /// OpenApiDiagnostic output - /// MappingModel - IEnumerable FromStream(Stream stream, out OpenApiDiagnostic diagnostic); - + /// MappingModel + IEnumerable FromStream(Stream stream, out OpenApiDiagnostic diagnostic); + /// - /// Generate from a file-path. + /// Generate from a . /// - /// The path to read the OpenApi/Swagger/V2/V3 or Raml file. + /// The source stream + /// Additional settings /// OpenApiDiagnostic output - /// MappingModel - IEnumerable FromFile(string path, out OpenApiDiagnostic diagnostic); - } + /// MappingModel + IEnumerable FromStream(Stream stream, 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 new file mode 100644 index 000000000..5a9b4c38a --- /dev/null +++ b/src/WireMock.Net.OpenApiParser/Mappers/OpenApiPathsMapper.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Writers; +using Newtonsoft.Json.Linq; +using WireMock.Admin.Mappings; +using WireMock.Net.OpenApiParser.Extensions; +using WireMock.Net.OpenApiParser.Settings; +using WireMock.Net.OpenApiParser.Types; +using WireMock.Net.OpenApiParser.Utils; + +namespace WireMock.Net.OpenApiParser.Mappers +{ + internal class OpenApiPathsMapper + { + private readonly WireMockOpenApiParserSettings _settings; + private readonly ExampleValueGenerator _exampleValueGenerator; + + public OpenApiPathsMapper(WireMockOpenApiParserSettings settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _exampleValueGenerator = new ExampleValueGenerator(settings); + } + + public IEnumerable ToMappingModels(OpenApiPaths paths) + { + return paths.Select(p => MapPath(p.Key, p.Value)).SelectMany(x => x); + } + + private IEnumerable MapPaths(OpenApiPaths paths) + { + return paths.Select(p => MapPath(p.Key, p.Value)).SelectMany(x => x); + } + + private IEnumerable MapPath(string path, OpenApiPathItem pathItem) + { + return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value)); + } + + private MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation) + { + var queryParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Query); + var pathParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Path); + var response = operation.Responses.FirstOrDefault(); + + TryGetContent(response.Value?.Content, out OpenApiMediaType responseContent, out string responseContentType); + var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema; + var responseExample = responseContent?.Example; + + var body = responseExample != null ? MapOpenApiAnyToJToken(responseExample) : MapSchemaToObject(responseSchema); + + if (int.TryParse(response.Key, out var httpStatusCode)) + { + httpStatusCode = 200; + } + + return new MappingModel + { + Guid = Guid.NewGuid(), + Request = new RequestModel + { + Methods = new[] { httpMethod }, + Path = MapPathWithParameters(path, pathParameters), + Params = MapQueryParameters(queryParameters) + }, + Response = new ResponseModel + { + StatusCode = httpStatusCode, + Headers = MapHeaders(responseContentType, response.Value?.Headers), + BodyAsJson = body + } + }; + } + + private bool TryGetContent(IDictionary contents, out OpenApiMediaType openApiMediaType, 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 object MapSchemaToObject(OpenApiSchema schema, string name = null) + { + if (schema == null) + { + return null; + } + + switch (schema.GetSchemaType()) + { + case SchemaType.Array: + var jArray = new JArray(); + for (int i = 0; i < _settings.NumberOfArrayItems; i++) + { + if (schema.Items.Properties.Count > 0) + { + var arrayItem = new JObject(); + foreach (var property in schema.Items.Properties) + { + var objectValue = MapSchemaToObject(property.Value, property.Key); + if (objectValue is JProperty jp) + { + arrayItem.Add(jp); + } + else + { + arrayItem.Add(new JProperty(property.Key, objectValue)); + } + } + + jArray.Add(arrayItem); + } + else + { + jArray.Add(MapSchemaToObject(schema.Items, name)); + } + } + + return jArray; + + case SchemaType.Boolean: + case SchemaType.Integer: + case SchemaType.Number: + case SchemaType.String: + return _exampleValueGenerator.GetExampleValue(schema); + + case SchemaType.Object: + var propertyAsJObject = new JObject(); + foreach (var schemaProperty in schema.Properties) + { + string propertyName = schemaProperty.Key; + var openApiSchema = schemaProperty.Value; + if (openApiSchema.GetSchemaType() == SchemaType.Object) + { + var mapped = MapSchemaToObject(schemaProperty.Value, schemaProperty.Key); + if (mapped is JProperty jp) + { + propertyAsJObject.Add(jp); + } + } + else + { + bool propertyIsNullable = openApiSchema.Nullable || (openApiSchema.TryGetXNullable(out bool x) && x); + + propertyAsJObject.Add(new JProperty(propertyName, _exampleValueGenerator.GetExampleValue(openApiSchema))); + } + } + + return name != null ? new JProperty(name, propertyAsJObject) : (JToken)propertyAsJObject; + + default: + return null; + } + } + + private string MapPathWithParameters(string path, IEnumerable parameters) + { + if (parameters == null) + { + return path; + } + + string newPath = path; + foreach (var parameter in parameters) + { + newPath = newPath.Replace($"{{{parameter.Name}}}", GetExampleValue(parameter.Schema, _settings.PathPatternToUse)); + } + + return newPath; + } + + 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); + + return JObject.Parse(outputString.ToString()); + } + + private IDictionary MapHeaders(string responseContentType, IDictionary headers) + { + var mappedHeaders = headers.ToDictionary( + item => item.Key, + item => GetExampleValue(null, _settings.HeaderPatternToUse) as object + ); + + if (!string.IsNullOrEmpty(responseContentType)) + { + if (!mappedHeaders.ContainsKey("Content-Type")) + { + mappedHeaders.Add("Content-Type", responseContentType); + } + else + { + mappedHeaders["Content-Type"] = responseContentType; + } + } + + return mappedHeaders.Keys.Any() ? mappedHeaders : null; + } + + private IList MapQueryParameters(IEnumerable queryParameters) + { + var list = queryParameters + .Select(qp => new ParamModel + { + Name = qp.Name, + Matchers = new[] + { + new MatcherModel + { + Name = "ExactMatcher", + Pattern = GetDefaultValueAsStringForSchemaType(qp.Schema) + } + } + }) + .ToList(); + + return list.Any() ? list : null; + } + + private string GetDefaultValueAsStringForSchemaType(OpenApiSchema schema) + { + var value = _exampleValueGenerator.GetExampleValue(schema); + + switch (value) + { + case string valueAsString: + return valueAsString; + + default: + return value.ToString(); + } + } + + private string GetExampleValue(OpenApiSchema schema, ExampleValueType type) + { + switch (type) + { + case ExampleValueType.Value: + return GetDefaultValueAsStringForSchemaType(schema); + + default: + return "*"; + } + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs new file mode 100644 index 000000000..c4d43ee95 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserExampleValues.cs @@ -0,0 +1,30 @@ +using System; + +namespace WireMock.Net.OpenApiParser.Settings +{ + /// + /// A class defining the example values to use for the different types. + /// + public class WireMockOpenApiParserExampleValues + { +#pragma warning disable 1591 + public bool Boolean { get; set; } = true; + + public int Integer { get; set; } = 42; + + public float Float { get; set; } = 4.2f; + + public double Double { get; set; } = 4.2d; + + public Func Date { get; set; } = () => System.DateTime.UtcNow.Date; + + public Func DateTime { get; set; } = () => System.DateTime.UtcNow; + + public byte[] Bytes { get; set; } = { 48, 49, 50 }; + + public object Object { get; set; } = "example-object"; + + public string String { get; set; } = "example-string"; +#pragma warning restore 1591 + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserSettings.cs b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserSettings.cs new file mode 100644 index 000000000..f02c98137 --- /dev/null +++ b/src/WireMock.Net.OpenApiParser/Settings/WireMockOpenApiParserSettings.cs @@ -0,0 +1,30 @@ +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 values to use + /// + public WireMockOpenApiParserExampleValues ExampleValues { get; } = new WireMockOpenApiParserExampleValues(); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/Types/ExampleValueType.cs b/src/WireMock.Net.OpenApiParser/Types/ExampleValueType.cs new file mode 100644 index 000000000..698a114db --- /dev/null +++ b/src/WireMock.Net.OpenApiParser/Types/ExampleValueType.cs @@ -0,0 +1,18 @@ +namespace WireMock.Net.OpenApiParser.Types +{ + /// + /// The example value to use + /// + public enum ExampleValueType + { + /// + /// Use a generated example value based on the SchemaType (default). + /// + Value, + + /// + /// Just use a Wildcard (*) character. + /// + Wildcard + } +} \ 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 87f1b25bf..1e03dc06b 100644 --- a/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs +++ b/src/WireMock.Net.OpenApiParser/Utils/ExampleValueGenerator.cs @@ -1,49 +1,57 @@ using System; using Microsoft.OpenApi.Models; using WireMock.Net.OpenApiParser.Extensions; +using WireMock.Net.OpenApiParser.Settings; using WireMock.Net.OpenApiParser.Types; namespace WireMock.Net.OpenApiParser.Utils { - internal static class ExampleValueGenerator + internal class ExampleValueGenerator { - public static object GetExampleValue(OpenApiSchema schema) + private readonly WireMockOpenApiParserSettings _settings; + + public ExampleValueGenerator(WireMockOpenApiParserSettings settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + public object GetExampleValue(OpenApiSchema schema) { switch (schema?.GetSchemaType()) { case SchemaType.Boolean: - return true; + return _settings.ExampleValues.Boolean; case SchemaType.Integer: - return 42; + return _settings.ExampleValues.Integer; case SchemaType.Number: switch (schema?.GetSchemaFormat()) { case SchemaFormat.Float: - return 4.2f; + return _settings.ExampleValues.Float; default: - return 4.2d; + return _settings.ExampleValues.Double; } default: switch (schema?.GetSchemaFormat()) { case SchemaFormat.Date: - return DateTimeUtils.ToRfc3339Date(DateTime.UtcNow); + return DateTimeUtils.ToRfc3339Date(_settings.ExampleValues.Date()); case SchemaFormat.DateTime: - return DateTimeUtils.ToRfc3339DateTime(DateTime.UtcNow); + return DateTimeUtils.ToRfc3339DateTime(_settings.ExampleValues.DateTime()); case SchemaFormat.Byte: - return new byte[] { 48, 49, 50 }; + return _settings.ExampleValues.Bytes; case SchemaFormat.Binary: - return "example-object"; + return _settings.ExampleValues.Object; default: - return "example-string"; + return _settings.ExampleValues.String; } } } diff --git a/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj b/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj index e3d507baf..45f887553 100644 --- a/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj +++ b/src/WireMock.Net.OpenApiParser/WireMock.Net.OpenApiParser.csproj @@ -12,6 +12,7 @@ ../WireMock.Net/WireMock.Net.snk true MIT + 8.0 @@ -24,14 +25,11 @@ + - - - - \ No newline at end of file diff --git a/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs b/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs index 44fed9457..d84672a82 100644 --- a/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs +++ b/src/WireMock.Net.OpenApiParser/WireMockOpenApiParser.cs @@ -1,34 +1,33 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using JetBrains.Annotations; -using Microsoft.OpenApi; -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; -using Microsoft.OpenApi.Writers; -using Newtonsoft.Json.Linq; using RamlToOpenApiConverter; using WireMock.Admin.Mappings; -using WireMock.Net.OpenApiParser.Extensions; -using WireMock.Net.OpenApiParser.Types; -using WireMock.Net.OpenApiParser.Utils; +using WireMock.Net.OpenApiParser.Mappers; +using WireMock.Net.OpenApiParser.Settings; namespace WireMock.Net.OpenApiParser { /// - /// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels. + /// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock.Net MappingModels. /// public class WireMockOpenApiParser : IWireMockOpenApiParser { - private const int ArrayItems = 3; - private readonly OpenApiStreamReader _reader = new OpenApiStreamReader(); - /// + /// [PublicAPI] public IEnumerable FromFile(string path, out OpenApiDiagnostic diagnostic) + { + return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic); + } + + /// + [PublicAPI] + public IEnumerable FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) { OpenApiDocument document; if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase)) @@ -42,248 +41,28 @@ public IEnumerable FromFile(string path, out OpenApiDiagnostic dia document = reader.Read(File.OpenRead(path), out diagnostic); } - return FromDocument(document); + return FromDocument(document, settings); } - /// + /// [PublicAPI] public IEnumerable FromStream(Stream stream, out OpenApiDiagnostic diagnostic) { return FromDocument(_reader.Read(stream, out diagnostic)); } - /// + /// [PublicAPI] - public IEnumerable FromDocument(OpenApiDocument openApiDocument) - { - return MapPaths(openApiDocument.Paths); - } - - private static IEnumerable MapPaths(OpenApiPaths paths) - { - return paths.Select(p => MapPath(p.Key, p.Value)).SelectMany(x => x); - } - - private static IEnumerable MapPath(string path, OpenApiPathItem pathItem) + public IEnumerable FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic) { - return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value)); + return FromDocument(_reader.Read(stream, out diagnostic), settings); } - private static MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation) - { - var queryParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Query); - var pathParameters = operation.Parameters.Where(p => p.In == ParameterLocation.Path); - var response = operation.Responses.FirstOrDefault(); - TryGetContent(response.Value?.Content, out OpenApiMediaType responseContent, out string responseContentType); - var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema; - var responseExample = responseContent?.Example; - - var body = responseExample != null ? MapOpenApiAnyToJToken(responseExample) : MapSchemaToObject(responseSchema); - - if (int.TryParse(response.Key, out var httpStatusCode)) - { - httpStatusCode = 200; - } - - return new MappingModel - { - Guid = Guid.NewGuid(), - Request = new RequestModel - { - Methods = new[] { httpMethod }, - Path = MapPathWithParameters(path, pathParameters), - Params = MapQueryParameters(queryParameters) - }, - Response = new ResponseModel - { - StatusCode = httpStatusCode, - Headers = MapHeaders(responseContentType, response.Value?.Headers), - BodyAsJson = body - } - }; - } - - private static bool TryGetContent(IDictionary contents, out OpenApiMediaType openApiMediaType, 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 static object MapSchemaToObject(OpenApiSchema schema, string name = null) - { - if (schema == null) - { - return null; - } - - switch (schema.GetSchemaType()) - { - case SchemaType.Array: - var jArray = new JArray(); - for (int i = 0; i < ArrayItems; i++) - { - if (schema.Items.Properties.Count > 0) - { - var arrayItem = new JObject(); - foreach (var property in schema.Items.Properties) - { - var objectValue = MapSchemaToObject(property.Value, property.Key); - if (objectValue is JProperty jp) - { - arrayItem.Add(jp); - } - else - { - arrayItem.Add(new JProperty(property.Key, objectValue)); - } - } - - jArray.Add(arrayItem); - } - else - { - jArray.Add(MapSchemaToObject(schema.Items, name)); - } - } - - return jArray; - - case SchemaType.Boolean: - case SchemaType.Integer: - case SchemaType.Number: - case SchemaType.String: - return ExampleValueGenerator.GetExampleValue(schema); - - case SchemaType.Object: - var propertyAsJObject = new JObject(); - foreach (var schemaProperty in schema.Properties) - { - string propertyName = schemaProperty.Key; - var openApiSchema = schemaProperty.Value; - if (openApiSchema.GetSchemaType() == SchemaType.Object) - { - var mapped = MapSchemaToObject(schemaProperty.Value, schemaProperty.Key); - if (mapped is JProperty jp) - { - propertyAsJObject.Add(jp); - } - } - else - { - bool propertyIsNullable = openApiSchema.Nullable || (openApiSchema.TryGetXNullable(out bool x) && x); - - propertyAsJObject.Add(new JProperty(propertyName, ExampleValueGenerator.GetExampleValue(openApiSchema))); - } - } - - return name != null ? new JProperty(name, propertyAsJObject) : (JToken)propertyAsJObject; - - default: - return null; - } - } - - private static string MapPathWithParameters(string path, IEnumerable parameters) - { - if (parameters == null) - { - return path; - } - - string newPath = path; - foreach (var parameter in parameters) - { - newPath = newPath.Replace($"{{{parameter.Name}}}", ExampleValueGenerator.GetExampleValue(parameter.Schema).ToString()); - } - - return newPath; - } - - private static 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); - - return JObject.Parse(outputString.ToString()); - } - } - - private static IDictionary MapHeaders(string responseContentType, IDictionary headers) - { - var mappedHeaders = headers.ToDictionary(item => item.Key, item => ExampleValueGenerator.GetExampleValue(null)); - if (!string.IsNullOrEmpty(responseContentType)) - { - if (!mappedHeaders.ContainsKey("Content-Type")) - { - mappedHeaders.Add("Content-Type", responseContentType); - } - else - { - mappedHeaders["Content-Type"] = responseContentType; - } - } - - return mappedHeaders.Keys.Any() ? mappedHeaders : null; - } - - private static IList MapQueryParameters(IEnumerable queryParameters) - { - var list = queryParameters - .Select(qp => new ParamModel - { - Name = qp.Name, - Matchers = new[] - { - new MatcherModel - { - Name = "ExactMatcher", - Pattern = GetDefaultValueAsStringForSchemaType(qp.Schema) - } - } - }) - .ToList(); - - return list.Any() ? list : null; - } - - private static string GetDefaultValueAsStringForSchemaType(OpenApiSchema schema) + /// + [PublicAPI] + public IEnumerable FromDocument(OpenApiDocument openApiDocument, WireMockOpenApiParserSettings settings = null) { - var value = ExampleValueGenerator.GetExampleValue(schema); - - switch (value) - { - case string valueAsString: - return valueAsString; - - default: - return value.ToString(); - } + return new OpenApiPathsMapper(settings).ToMappingModels(openApiDocument.Paths); } } } \ No newline at end of file