Skip to content

Commit fb6ea09

Browse files
Merge pull request #985 from microsoft/mk/enhancement-create-hash-code
Compute hash of OpenApi document
2 parents c1b173e + 2539576 commit fb6ea09

File tree

12 files changed

+112
-89
lines changed

12 files changed

+112
-89
lines changed

src/Microsoft.OpenApi.Hidi/OpenApiService.cs

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -356,57 +356,7 @@ public static OpenApiDocument FixReferences(OpenApiDocument document)
356356

357357
return doc;
358358
}
359-
360-
private static async Task<Stream> GetStream(string input, ILogger logger)
361-
{
362-
var stopwatch = new Stopwatch();
363-
stopwatch.Start();
364-
365-
Stream stream;
366-
if (input.StartsWith("http"))
367-
{
368-
try
369-
{
370-
var httpClientHandler = new HttpClientHandler()
371-
{
372-
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
373-
};
374-
using var httpClient = new HttpClient(httpClientHandler)
375-
{
376-
DefaultRequestVersion = HttpVersion.Version20
377-
};
378-
stream = await httpClient.GetStreamAsync(input);
379-
}
380-
catch (HttpRequestException ex)
381-
{
382-
logger.LogError($"Could not download the file at {input}, reason{ex}");
383-
return null;
384-
}
385-
}
386-
else
387-
{
388-
try
389-
{
390-
var fileInput = new FileInfo(input);
391-
stream = fileInput.OpenRead();
392-
}
393-
catch (Exception ex) when (ex is FileNotFoundException ||
394-
ex is PathTooLongException ||
395-
ex is DirectoryNotFoundException ||
396-
ex is IOException ||
397-
ex is UnauthorizedAccessException ||
398-
ex is SecurityException ||
399-
ex is NotSupportedException)
400-
{
401-
logger.LogError($"Could not open the file at {input}, reason: {ex.Message}");
402-
return null;
403-
}
404-
}
405-
stopwatch.Stop();
406-
logger.LogTrace("{timestamp}ms: Read file {input}", stopwatch.ElapsedMilliseconds, input);
407-
return stream;
408-
}
409-
359+
410360
/// <summary>
411361
/// Takes in a file stream, parses the stream into a JsonDocument and gets a list of paths and Http methods
412362
/// </summary>
@@ -462,34 +412,6 @@ private static Dictionary<string, List<string>> EnumerateJsonDocument(JsonElemen
462412
return paths;
463413
}
464414

465-
/// <summary>
466-
/// Fixes the references in the resulting OpenApiDocument.
467-
/// </summary>
468-
/// <param name="document"> The converted OpenApiDocument.</param>
469-
/// <returns> A valid OpenApiDocument instance.</returns>
470-
// private static OpenApiDocument FixReferences2(OpenApiDocument document)
471-
// {
472-
// // This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance.
473-
// // So we write it out, and read it back in again to fix it up.
474-
475-
// OpenApiDocument document;
476-
// logger.LogTrace("Parsing the OpenApi file");
477-
// var result = await new OpenApiStreamReader(new OpenApiReaderSettings
478-
// {
479-
// RuleSet = ValidationRuleSet.GetDefaultRuleSet(),
480-
// BaseUrl = new Uri(openapi)
481-
// }
482-
// ).ReadAsync(stream);
483-
484-
// document = result.OpenApiDocument;
485-
// var context = result.OpenApiDiagnostic;
486-
// var sb = new StringBuilder();
487-
// document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb)));
488-
// var doc = new OpenApiStringReader().Read(sb.ToString(), out _);
489-
490-
// return doc;
491-
// }
492-
493415
/// <summary>
494416
/// Reads stream from file system or makes HTTP request depending on the input string
495417
/// </summary>

src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic
7979
{
8080
diagnostic.Warnings.Add(item);
8181
}
82-
8382
}
8483

8584
return document;

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System;
55
using System.Collections.Generic;
6+
using System.IO;
67
using System.Linq;
8+
using System.Security.Cryptography;
9+
using System.Text;
710
using Microsoft.OpenApi.Exceptions;
811
using Microsoft.OpenApi.Interfaces;
912
using Microsoft.OpenApi.Services;
@@ -62,6 +65,11 @@ public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible
6265
/// </summary>
6366
public IDictionary<string, IOpenApiExtension> Extensions { get; set; } = new Dictionary<string, IOpenApiExtension>();
6467

68+
/// <summary>
69+
/// The unique hash code of the generated OpenAPI document
70+
/// </summary>
71+
public string HashCode => GenerateHashValue(this);
72+
6573
/// <summary>
6674
/// Parameter-less constructor
6775
/// </summary>
@@ -375,6 +383,40 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference)
375383
return ResolveReference(reference, false);
376384
}
377385

386+
/// <summary>
387+
/// Takes in an OpenApi document instance and generates its hash value
388+
/// </summary>
389+
/// <param name="doc">The OpenAPI description to hash.</param>
390+
/// <returns>The hash value.</returns>
391+
public static string GenerateHashValue(OpenApiDocument doc)
392+
{
393+
using HashAlgorithm sha = SHA512.Create();
394+
using var cryptoStream = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write);
395+
using var streamWriter = new StreamWriter(cryptoStream);
396+
397+
var openApiJsonWriter = new OpenApiJsonWriter(streamWriter, new OpenApiJsonWriterSettings { Terse = true });
398+
doc.SerializeAsV3(openApiJsonWriter);
399+
openApiJsonWriter.Flush();
400+
401+
cryptoStream.FlushFinalBlock();
402+
var hash = sha.Hash;
403+
404+
return ConvertByteArrayToString(hash);
405+
}
406+
407+
private static string ConvertByteArrayToString(byte[] hash)
408+
{
409+
// Build the final string by converting each byte
410+
// into hex and appending it to a StringBuilder
411+
StringBuilder sb = new StringBuilder();
412+
for (int i = 0; i < hash.Length; i++)
413+
{
414+
sb.Append(hash[i].ToString("X2"));
415+
}
416+
417+
return sb.ToString();
418+
}
419+
378420
/// <summary>
379421
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="OpenApiReference"/> object
380422
/// </summary>

test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>net6.0</TargetFrameworks>
44
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>

test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public void ShouldParseProducesInAnyOrder()
208208
{
209209
Type = ReferenceType.Schema,
210210
Id = "Error",
211-
HostDocument= doc
211+
HostDocument = doc
212212
},
213213
Properties = new Dictionary<string, OpenApiSchema>()
214214
{
@@ -407,7 +407,7 @@ public void ShouldAssignSchemaToAllResponses()
407407
{
408408
Id = "Error",
409409
Type = ReferenceType.Schema,
410-
HostDocument= document
410+
HostDocument = document
411411
}
412412
};
413413
var responses = document.Paths["/items"].Operations[OperationType.Get].Responses;

test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ public T Clone<T>(T element) where T : IOpenApiSerializable
3434
{
3535
IOpenApiWriter writer;
3636
var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture);
37-
writer = new OpenApiJsonWriter(streamWriter, new OpenApiJsonWriterSettings() {
38-
InlineLocalReferences = true});
37+
writer = new OpenApiJsonWriter(streamWriter, new OpenApiJsonWriterSettings()
38+
{
39+
InlineLocalReferences = true
40+
});
3941
element.SerializeAsV3(writer);
4042
writer.Flush();
4143
stream.Position = 0;
@@ -48,7 +50,7 @@ public T Clone<T>(T element) where T : IOpenApiSerializable
4850
}
4951
}
5052

51-
public OpenApiSecurityScheme CloneSecurityScheme(OpenApiSecurityScheme element)
53+
public OpenApiSecurityScheme CloneSecurityScheme(OpenApiSecurityScheme element)
5254
{
5355
using (var stream = new MemoryStream())
5456
{

test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ public void ParseBasicSchemaWithReferenceShouldSucceed()
423423
}
424424
}
425425
}
426-
},options => options.Excluding(m => m.Name == "HostDocument"));
426+
}, options => options.Excluding(m => m.Name == "HostDocument"));
427427
}
428428
}
429429

test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@
3636
</ItemGroup>
3737

3838
<ItemGroup>
39+
<EmbeddedResource Include="Models\Samples\sampleDocument.yaml">
40+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
41+
</EmbeddedResource>
42+
<EmbeddedResource Include="Models\Samples\sampleDocumentWithWhiteSpaces.yaml">
43+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
44+
</EmbeddedResource>
45+
</ItemGroup>
46+
47+
<ItemGroup>
48+
3949
<None Update="PublicApi\PublicApi.approved.txt" CopyToOutputDirectory="Always" />
4050
</ItemGroup>
51+
52+
<ItemGroup>
53+
<Folder Include="Models\Samples\" />
54+
</ItemGroup>
4155
</Project>

test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System;
@@ -10,6 +10,7 @@
1010
using Microsoft.OpenApi.Extensions;
1111
using Microsoft.OpenApi.Interfaces;
1212
using Microsoft.OpenApi.Models;
13+
using Microsoft.OpenApi.Readers;
1314
using Microsoft.OpenApi.Writers;
1415
using VerifyXunit;
1516
using Xunit;
@@ -1314,5 +1315,32 @@ public void SerializeRelativeRootPathWithHostAsV2JsonWorks()
13141315
actual.Should().Be(expected);
13151316
}
13161317

1318+
[Fact]
1319+
public void TestHashCodesForSimilarOpenApiDocuments()
1320+
{
1321+
// Arrange
1322+
var sampleFolderPath = "Models/Samples/";
1323+
1324+
var doc1 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocument.yaml"));
1325+
var doc2 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocument.yaml"));
1326+
var doc3 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocumentWithWhiteSpaces.yaml"));
1327+
1328+
// Act && Assert
1329+
/*
1330+
Test whether reading in two similar documents yield the same hash code,
1331+
And reading in similar documents(one has a whitespace) yields the same hash code as the result is terse
1332+
*/
1333+
Assert.True(doc1.HashCode != null && doc2.HashCode != null && doc1.HashCode.Equals(doc2.HashCode));
1334+
Assert.Equal(doc1.HashCode, doc3.HashCode);
1335+
}
1336+
1337+
private static OpenApiDocument ParseInputFile(string filePath)
1338+
{
1339+
// Read in the input yaml file
1340+
using FileStream stream = File.OpenRead(filePath);
1341+
var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic);
1342+
1343+
return openApiDoc;
1344+
}
13171345
}
13181346
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
openapi : 3.0.0
2+
info:
3+
title: Simple Document
4+
version: 0.9.1
5+
paths: {}

0 commit comments

Comments
 (0)