diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 5e8ddbd3b..2f1b6b9b5 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -266,7 +266,6 @@ stages: echo "$artifactName" echo "$artifactVersion" displayName: 'Fetch Artifact Name' - - task: NuGetCommand@2 displayName: 'NuGet push' inputs: @@ -276,6 +275,7 @@ stages: publishFeedCredentials: 'OpenAPI Nuget Connection' - task: GitHubRelease@1 displayName: 'GitHub release (edit)' + condition: succeededOrFailed() inputs: gitHubConnection: 'Github-MaggieKimani1' action: edit @@ -285,6 +285,12 @@ stages: releaseNotesSource: inline assets: '$(Pipeline.Workspace)\**\*.exe' changeLogType: issueBased + changeLogLabels: '[ + { "label" : "feature-work", "feature", "displayName" : "New Features", "state" : "closed" }, + { "label" : "enhancement", "V2-Enhancement", "displayName" : "Enhancements", "state" : "closed" }, + { "label" : "bug", "bug-fix", "displayName" : "Bugs", "state" : "closed" }, + { "label" : "documentation", "doc", "displayName" : "Documentation", "state" : "closed"}, + { "label" : "dependencies", "displayName" : "Package Updates", "state" : "closed" }]' - deployment: deploy_lib dependsOn: [] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..c5b4cab90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the current behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots/Code Snippets** +If applicable, add screenshots of the stack trace or a code snippet to help explain your problem. +If applicable, add a link to your project + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..021458556 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 6045d85b6..084738bac 100644 --- a/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index c37c9479d..461ca50be 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -356,57 +356,7 @@ public static OpenApiDocument FixReferences(OpenApiDocument document) return doc; } - - private static async Task GetStream(string input, ILogger logger) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - Stream stream; - if (input.StartsWith("http")) - { - try - { - var httpClientHandler = new HttpClientHandler() - { - SslProtocols = System.Security.Authentication.SslProtocols.Tls12, - }; - using var httpClient = new HttpClient(httpClientHandler) - { - DefaultRequestVersion = HttpVersion.Version20 - }; - stream = await httpClient.GetStreamAsync(input); - } - catch (HttpRequestException ex) - { - logger.LogError($"Could not download the file at {input}, reason{ex}"); - return null; - } - } - else - { - try - { - var fileInput = new FileInfo(input); - stream = fileInput.OpenRead(); - } - catch (Exception ex) when (ex is FileNotFoundException || - ex is PathTooLongException || - ex is DirectoryNotFoundException || - ex is IOException || - ex is UnauthorizedAccessException || - ex is SecurityException || - ex is NotSupportedException) - { - logger.LogError($"Could not open the file at {input}, reason: {ex.Message}"); - return null; - } - } - stopwatch.Stop(); - logger.LogTrace("{timestamp}ms: Read file {input}", stopwatch.ElapsedMilliseconds, input); - return stream; - } - + /// /// Takes in a file stream, parses the stream into a JsonDocument and gets a list of paths and Http methods /// @@ -462,34 +412,6 @@ private static Dictionary> EnumerateJsonDocument(JsonElemen return paths; } - /// - /// Fixes the references in the resulting OpenApiDocument. - /// - /// The converted OpenApiDocument. - /// A valid OpenApiDocument instance. - // private static OpenApiDocument FixReferences2(OpenApiDocument document) - // { - // // This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance. - // // So we write it out, and read it back in again to fix it up. - - // OpenApiDocument document; - // logger.LogTrace("Parsing the OpenApi file"); - // var result = await new OpenApiStreamReader(new OpenApiReaderSettings - // { - // RuleSet = ValidationRuleSet.GetDefaultRuleSet(), - // BaseUrl = new Uri(openapi) - // } - // ).ReadAsync(stream); - - // document = result.OpenApiDocument; - // var context = result.OpenApiDiagnostic; - // var sb = new StringBuilder(); - // document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb))); - // var doc = new OpenApiStringReader().Read(sb.ToString(), out _); - - // return doc; - // } - /// /// Reads stream from file system or makes HTTP request depending on the input string /// diff --git a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj index c92eaf766..9ca5f6cfe 100644 --- a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj +++ b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj @@ -10,7 +10,7 @@ Microsoft Microsoft.OpenApi.Readers Microsoft.OpenApi.Readers - 1.4.0-preview2 + 1.4.0-preview3 OpenAPI.NET Readers for JSON and YAML documents © Microsoft Corporation. All rights reserved. OpenAPI .NET @@ -34,7 +34,7 @@ - + diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index aae09ec86..3aedafbf1 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -79,7 +79,6 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic { diagnostic.Warnings.Add(item); } - } return document; diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs index a3bda05e1..1cf5b7ae8 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs @@ -60,7 +60,7 @@ internal static partial class OpenApiV2Deserializer "consumes", (o, n) => { var consumes = n.CreateSimpleList(s => s.GetScalarValue()); if (consumes.Count > 0) { - n.Context.SetTempStorage(TempStorageKeys.OperationConsumes,consumes); + n.Context.SetTempStorage(TempStorageKeys.OperationConsumes,consumes); } } }, diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs index 718dcec04..33c9d7c6f 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs @@ -107,30 +107,6 @@ private static ReferenceType ParseReferenceType(string referenceTypeName) } } - private static string GetReferenceTypeV2Name(ReferenceType referenceType) - { - switch (referenceType) - { - case ReferenceType.Schema: - return "definitions"; - - case ReferenceType.Parameter: - return "parameters"; - - case ReferenceType.Response: - return "responses"; - - case ReferenceType.Tag: - return "tags"; - - case ReferenceType.SecurityScheme: - return "securityDefinitions"; - - default: - throw new ArgumentException(); - } - } - private static ReferenceType GetReferenceTypeV2FromName(string referenceType) { switch (referenceType) diff --git a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj index a768312e6..1bbfcb987 100644 --- a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj +++ b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj @@ -11,7 +11,7 @@ Microsoft Microsoft.OpenApi Microsoft.OpenApi - 1.4.0-preview2 + 1.4.0-preview3 .NET models with JSON and YAML writers for OpenAPI specification © Microsoft Corporation. All rights reserved. OpenAPI .NET diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 01edcebba..836e45dd8 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -1,9 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Security.Cryptography; +using System.Text; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Services; @@ -62,6 +65,11 @@ public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible /// public IDictionary Extensions { get; set; } = new Dictionary(); + /// + /// The unique hash code of the generated OpenAPI document + /// + public string HashCode => GenerateHashValue(this); + /// /// Parameter-less constructor /// @@ -375,6 +383,40 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference) return ResolveReference(reference, false); } + /// + /// Takes in an OpenApi document instance and generates its hash value + /// + /// The OpenAPI description to hash. + /// The hash value. + public static string GenerateHashValue(OpenApiDocument doc) + { + using HashAlgorithm sha = SHA512.Create(); + using var cryptoStream = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write); + using var streamWriter = new StreamWriter(cryptoStream); + + var openApiJsonWriter = new OpenApiJsonWriter(streamWriter, new OpenApiJsonWriterSettings { Terse = true }); + doc.SerializeAsV3(openApiJsonWriter); + openApiJsonWriter.Flush(); + + cryptoStream.FlushFinalBlock(); + var hash = sha.Hash; + + return ConvertByteArrayToString(hash); + } + + private static string ConvertByteArrayToString(byte[] hash) + { + // Build the final string by converting each byte + // into hex and appending it to a StringBuilder + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("X2")); + } + + return sb.ToString(); + } + /// /// Load the referenced object from a object /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index ba0af7317..7983a243e 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -253,9 +253,11 @@ public void SerializeAsV2(IOpenApiWriter writer) { foreach (var property in RequestBody.Content.First().Value.Schema.Properties) { - var paramName = property.Key; var paramSchema = property.Value; - if (paramSchema.Type == "string" && paramSchema.Format == "binary") { + if ("string".Equals(paramSchema.Type, StringComparison.OrdinalIgnoreCase) + && ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase) + || "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase))) + { paramSchema.Type = "file"; paramSchema.Format = null; } diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 3886a5555..8734c19a2 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -630,6 +630,10 @@ internal void WriteAsSchemaProperties( } // format + Format ??= AllOf?.FirstOrDefault(static x => x.Format != null)?.Format ?? + AnyOf?.FirstOrDefault(static x => x.Format != null)?.Format ?? + OneOf?.FirstOrDefault(static x => x.Format != null)?.Format; + writer.WriteProperty(OpenApiConstants.Format, Format); // title @@ -695,17 +699,17 @@ internal void WriteAsSchemaProperties( // allOf writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV2(w)); - // If there isn't already an AllOf, and the schema contains a oneOf or anyOf write an allOf with the first + // If there isn't already an allOf, and the schema contains a oneOf or anyOf write an allOf with the first // schema in the list as an attempt to guess at a graceful downgrade situation. if (AllOf == null || AllOf.Count == 0) { // anyOf (Not Supported in V2) - Write the first schema only as an allOf. - writer.WriteOptionalCollection(OpenApiConstants.AllOf, AnyOf.Take(1), (w, s) => s.SerializeAsV2(w)); + writer.WriteOptionalCollection(OpenApiConstants.AllOf, AnyOf?.Take(1), (w, s) => s.SerializeAsV2(w)); if (AnyOf == null || AnyOf.Count == 0) { // oneOf (Not Supported in V2) - Write the first schema only as an allOf. - writer.WriteOptionalCollection(OpenApiConstants.AllOf, OneOf.Take(1), (w, s) => s.SerializeAsV2(w)); + writer.WriteOptionalCollection(OpenApiConstants.AllOf, OneOf?.Take(1), (w, s) => s.SerializeAsV2(w)); } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs index 1639e6248..a8ed2e93c 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Properties; using System.Collections.Generic; @@ -11,7 +10,6 @@ namespace Microsoft.OpenApi.Validations.Rules /// /// The validation rules for . /// - [OpenApiRule] public static class OpenApiSchemaRules { @@ -70,15 +68,74 @@ public static class OpenApiSchemaRules if (schema.Reference != null && schema.Discriminator != null) { - if (!schema.Required.Contains(schema.Discriminator?.PropertyName)) + var discriminatorName = schema.Discriminator?.PropertyName; + + if (!ValidateChildSchemaAgainstDiscriminator(schema, discriminatorName)) { context.CreateError(nameof(ValidateSchemaDiscriminator), - string.Format(SRResource.Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator, - schema.Reference.Id, schema.Discriminator.PropertyName)); + string.Format(SRResource.Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator, + schema.Reference.Id, discriminatorName)); } } context.Exit(); }); + + /// + /// Validates the property name in the discriminator against the ones present in the children schema + /// + /// The parent schema. + /// Adds support for polymorphism. The discriminator is an object name that is used to differentiate + /// between other schemas which may satisfy the payload description. + public static bool ValidateChildSchemaAgainstDiscriminator(OpenApiSchema schema, string discriminatorName) + { + if (!schema.Required?.Contains(discriminatorName) ?? false) + { + // recursively check nested schema.OneOf, schema.AnyOf or schema.AllOf and their required fields for the discriminator + if (schema.OneOf.Count != 0) + { + return TraverseSchemaElements(discriminatorName, schema.OneOf); + } + if (schema.AnyOf.Count != 0) + { + return TraverseSchemaElements(discriminatorName, schema.AnyOf); + } + if (schema.AllOf.Count != 0) + { + return TraverseSchemaElements(discriminatorName, schema.AllOf); + } + } + else + { + return true; + } + + return false; + } + + /// + /// Traverses the schema elements and checks whether the schema contains the discriminator. + /// + /// Adds support for polymorphism. The discriminator is an object name that is used to differentiate + /// between other schemas which may satisfy the payload description. + /// The child schema. + /// + public static bool TraverseSchemaElements(string discriminatorName, IList childSchema) + { + foreach (var childItem in childSchema) + { + if ((!childItem.Properties?.ContainsKey(discriminatorName) ?? false) && + (!childItem.Required?.Contains(discriminatorName) ?? false)) + { + return ValidateChildSchemaAgainstDiscriminator(childItem, discriminatorName); + } + else + { + return true; + } + } + + return false; + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 83e5482a7..63b7a2a1f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -1,4 +1,4 @@ - + net6.0 false @@ -250,12 +250,12 @@ - + - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs index 39bc0db80..fcf0471ea 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs @@ -208,7 +208,7 @@ public void ShouldParseProducesInAnyOrder() { Type = ReferenceType.Schema, Id = "Error", - HostDocument= doc + HostDocument = doc }, Properties = new Dictionary() { @@ -407,7 +407,7 @@ public void ShouldAssignSchemaToAllResponses() { Id = "Error", Type = ReferenceType.Schema, - HostDocument= document + HostDocument = document } }; var responses = document.Paths["/items"].Operations[OperationType.Get].Responses; diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index f1d8b805f..6fbb7065a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -34,8 +34,10 @@ public T Clone(T element) where T : IOpenApiSerializable { IOpenApiWriter writer; var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture); - writer = new OpenApiJsonWriter(streamWriter, new OpenApiJsonWriterSettings() { - InlineLocalReferences = true}); + writer = new OpenApiJsonWriter(streamWriter, new OpenApiJsonWriterSettings() + { + InlineLocalReferences = true + }); element.SerializeAsV3(writer); writer.Flush(); stream.Position = 0; @@ -48,7 +50,7 @@ public T Clone(T element) where T : IOpenApiSerializable } } - public OpenApiSecurityScheme CloneSecurityScheme(OpenApiSecurityScheme element) + public OpenApiSecurityScheme CloneSecurityScheme(OpenApiSecurityScheme element) { using (var stream = new MemoryStream()) { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs index 9bdafeba6..0101d9c6e 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs @@ -423,7 +423,7 @@ public void ParseBasicSchemaWithReferenceShouldSucceed() } } } - },options => options.Excluding(m => m.Name == "HostDocument")); + }, options => options.Excluding(m => m.Name == "HostDocument")); } } diff --git a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj index ef9886bb9..114ba749c 100644 --- a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj +++ b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj @@ -8,7 +8,7 @@ - + diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 022b0e5dc..5b476500c 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -16,11 +16,11 @@ - + - - + + all @@ -36,6 +36,20 @@ + + Always + + + Always + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 10cadd597..cd4cc2b5a 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -10,6 +10,7 @@ using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; using Microsoft.OpenApi.Writers; using VerifyXunit; using Xunit; @@ -1314,5 +1315,32 @@ public void SerializeRelativeRootPathWithHostAsV2JsonWorks() actual.Should().Be(expected); } + [Fact] + public void TestHashCodesForSimilarOpenApiDocuments() + { + // Arrange + var sampleFolderPath = "Models/Samples/"; + + var doc1 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocument.yaml")); + var doc2 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocument.yaml")); + var doc3 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocumentWithWhiteSpaces.yaml")); + + // Act && Assert + /* + Test whether reading in two similar documents yield the same hash code, + And reading in similar documents(one has a whitespace) yields the same hash code as the result is terse + */ + Assert.True(doc1.HashCode != null && doc2.HashCode != null && doc1.HashCode.Equals(doc2.HashCode)); + Assert.Equal(doc1.HashCode, doc3.HashCode); + } + + private static OpenApiDocument ParseInputFile(string filePath) + { + // Read in the input yaml file + using FileStream stream = File.OpenRead(filePath); + var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + + return openApiDoc; + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index ae13944e6..429129c1e 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -422,5 +422,46 @@ public async Task SerializeSchemaWRequiredPropertiesAsV2JsonWorksAsync(bool prod // Assert await Verifier.Verify(actual).UseParameters(produceTerseOutput); } + + [Fact] + public void SerializeAsV2ShouldSetFormatPropertyInParentSchemaIfPresentInChildrenSchema() + { + // Arrange + var schema = new OpenApiSchema() + { + OneOf = new List + { + new OpenApiSchema + { + Type = "number", + Format = "decimal" + }, + new OpenApiSchema { Type = "string" }, + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var openApiJsonWriter = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = false }); + + // Act + // Serialize as V2 + schema.SerializeAsV2(openApiJsonWriter); + openApiJsonWriter.Flush(); + + var v2Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV2Schema = @"{ + ""format"": ""decimal"", + ""allOf"": [ + { + ""format"": ""decimal"", + ""type"": ""number"" + } + ] +}".MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV2Schema, v2Schema); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/Samples/sampleDocument.yaml b/test/Microsoft.OpenApi.Tests/Models/Samples/sampleDocument.yaml new file mode 100644 index 000000000..34153a5f5 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/Samples/sampleDocument.yaml @@ -0,0 +1,5 @@ +openapi : 3.0.0 +info: + title: Simple Document + version: 0.9.1 +paths: {} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/Samples/sampleDocumentWithWhiteSpaces.yaml b/test/Microsoft.OpenApi.Tests/Models/Samples/sampleDocumentWithWhiteSpaces.yaml new file mode 100644 index 000000000..5f31baa0e --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/Samples/sampleDocumentWithWhiteSpaces.yaml @@ -0,0 +1,9 @@ +openapi : 3.0.0 + +info: + title: Simple Document + + version: 0.9.1 + +paths: {} + diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 0753ebd74..745d91d43 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -525,6 +525,7 @@ namespace Microsoft.OpenApi.Models public Microsoft.OpenApi.Models.OpenApiComponents Components { get; set; } public System.Collections.Generic.IDictionary Extensions { get; set; } public Microsoft.OpenApi.Models.OpenApiExternalDocs ExternalDocs { get; set; } + public string HashCode { get; } public Microsoft.OpenApi.Models.OpenApiInfo Info { get; set; } public Microsoft.OpenApi.Models.OpenApiPaths Paths { get; set; } public System.Collections.Generic.IList SecurityRequirements { get; set; } @@ -535,6 +536,7 @@ namespace Microsoft.OpenApi.Models public System.Collections.Generic.IEnumerable ResolveReferences() { } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } + public static string GenerateHashValue(Microsoft.OpenApi.Models.OpenApiDocument doc) { } } public class OpenApiEncoding : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiExtensible, Microsoft.OpenApi.Interfaces.IOpenApiSerializable { @@ -1326,6 +1328,8 @@ namespace Microsoft.OpenApi.Validations.Rules { public static Microsoft.OpenApi.Validations.ValidationRule SchemaMismatchedDataType { get; } public static Microsoft.OpenApi.Validations.ValidationRule ValidateSchemaDiscriminator { get; } + public static bool TraverseSchemaElements(string discriminatorName, System.Collections.Generic.IList childSchema) { } + public static bool ValidateChildSchemaAgainstDiscriminator(Microsoft.OpenApi.Models.OpenApiSchema schema, string discriminatorName) { } } [Microsoft.OpenApi.Validations.Rules.OpenApiRule] public static class OpenApiServerRules diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs index d239e15a1..04acf7737 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs @@ -268,5 +268,60 @@ public void ValidateSchemaRequiredFieldListMustContainThePropertySpecifiedInTheD "schema1", "property1")) }); } + + [Fact] + public void ValidateOneOfSchemaPropertyNameContainsPropertySpecifiedInTheDiscriminator() + { + // Arrange + var components = new OpenApiComponents + { + Schemas = + { + { + "Person", + new OpenApiSchema + { + Type = "array", + Discriminator = new OpenApiDiscriminator + { + PropertyName = "type" + }, + OneOf = new List + { + new OpenApiSchema + { + Properties = + { + { + "type", + new OpenApiSchema + { + Type = "array" + } + } + }, + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "Person" + } + } + }, + Reference = new OpenApiReference { Id = "Person" } + } + } + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(components); + + var errors = validator.Errors; + + //Assert + errors.Should().BeEmpty(); + } } }