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/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/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/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 4faadc3f6..94432db9a 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 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.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index a6ba76259..872447cc9 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -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/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 b320f8b10..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 {