Skip to content

Commit 146cab4

Browse files
committed
Added implementation for detecting open Api changes
1 parent a9c3a93 commit 146cab4

File tree

9 files changed

+275
-0
lines changed

9 files changed

+275
-0
lines changed

.azure-pipelines/common-templates/install-tools.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ steps:
1212
inputs:
1313
debugMode: false
1414
version: 7.x
15+
16+
- task: UseDotNet@2
17+
displayName: Use .NET SDK
18+
inputs:
19+
debugMode: false
20+
version: 8.x
1521

1622
- task: NuGetToolInstaller@1
1723
displayName: Install Nuget

src/Authentication/Authentication.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Graph.Authenticat
99
EndProject
1010
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Graph.Authentication.Core", "Authentication.Core\Microsoft.Graph.Authentication.Core.csproj", "{50050576-74B8-4507-B1FE-C47740BB3B71}"
1111
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiInfoGenerator", "..\..\tools\OpenApiInfoGenerator\OpenApiInfoGenerator\OpenApiInfoGenerator.csproj", "{6437E29A-E398-48EE-B7BE-27A8C922F13E}"
13+
EndProject
1214
Global
1315
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1416
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,10 @@ Global
2729
{50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
2830
{50050576-74B8-4507-B1FE-C47740BB3B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
2931
{50050576-74B8-4507-B1FE-C47740BB3B71}.Release|Any CPU.Build.0 = Release|Any CPU
32+
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|Any CPU.Build.0 = Release|Any CPU
3036
EndGlobalSection
3137
GlobalSection(SolutionProperties) = preSolution
3238
HideSolutionNode = FALSE
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace openapiinfo;
2+
3+
public static class FileHandler
4+
{
5+
private const string OpenApiFolderName = "OpenApiDocs";
6+
private static string? AssemblyLocation = Path.GetDirectoryName(typeof(FileHandler).Assembly.Location);
7+
8+
public static string? GetOpenApiFolder()
9+
{
10+
if (AssemblyLocation is null)
11+
{
12+
throw new InvalidOperationException("Assembly location is null.");
13+
}
14+
15+
return Path.Combine(AssemblyLocation, $"../../../../../../{OpenApiFolderName}");
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace openapiinfo;
2+
public class Model
3+
{
4+
public PathInfo? PathInfo { get; set; }
5+
public MethodInfo? MethodInfo { get; set; }
6+
7+
}
8+
public class PathInfo
9+
{
10+
public string? Path { get; set; }
11+
public string? Module { get; set; }
12+
}
13+
public class MethodInfo
14+
{
15+
public string? OperationId { get; set; }
16+
public string? Method { get; set; }
17+
public List<Parameters>? Parameters { get; set; }
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
Param(
4+
[string] $v1_OpenAPIErrorFilePath = (Join-Path $PSScriptRoot "..\..\..\openApiDocs\v1.0\OpenApiInfo\"),
5+
[string] $beta_OpenAPIErrorFilePath = (Join-Path $PSScriptRoot "..\..\..\openApiDocs\beta\OpenApiInfo\"),
6+
[string] $OpenAPIErrorFileName = "openAPIErrors"
7+
)
8+
9+
function Start-Validation {
10+
Write-Host "Validating OpenAPI errors..."
11+
$V1_OpenAPIErrorFile = Join-Path $v1_OpenAPIErrorFilePath "$OpenAPIErrorFileName.csv"
12+
$Beta_OpenAPIErrorFile = Join-Path $beta_OpenAPIErrorFilePath "$OpenAPIErrorFileName.csv"
13+
14+
$GraphMapping = @{
15+
"v1.0" = $V1_OpenAPIErrorFile
16+
"beta" = $Beta_OpenAPIErrorFile
17+
}
18+
$GraphMapping.Keys | ForEach-Object {
19+
$GraphProfile = $_
20+
$ErrorFile = $GraphMapping[$GraphProfile]
21+
if (Test-Path $ErrorFile) {
22+
#Check if the file is empty
23+
$ErrorFileContent = Get-Content -Path $ErrorFile -Raw
24+
if ([string]::IsNullOrEmpty($ErrorFileContent)) {
25+
Write-Host "No unnecessary errors found in $GraphProfile OpenAPI file."
26+
}
27+
else {
28+
#Throw error to stop the pipeline run
29+
Throw "Unnecessary errors found in $GraphProfile OpenAPI file."
30+
31+
}
32+
}
33+
else {
34+
Write-Host "No errors found in $GraphProfile OpenAPI file."
35+
}
36+
}
37+
38+
}
39+
dotnet run
40+
Start-Validation
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.14" />
11+
<PackageReference Include="Microsoft.OpenApi" Version="1.6.14" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Text.Json;
2+
using openapiinfo;
3+
public class OriginalMetadata
4+
{
5+
private string url;
6+
private Lazy<IList<Model>> OpenApiInfo;
7+
public OriginalMetadata(string endpoint)
8+
{
9+
url = endpoint;
10+
Lazy<IList<Model>> OpenApiInfo = new(
11+
() =>
12+
{
13+
using var httpClient = new HttpClient();
14+
Uri uri = new Uri(url);
15+
using var stream = httpClient.GetStreamAsync(uri).GetAwaiter().GetResult();
16+
var result = JsonSerializer.Deserialize<IList<Model>>(stream);
17+
return result ?? new List<Model>(); // Add null check and return an empty list if result is null
18+
},
19+
LazyThreadSafetyMode.PublicationOnly
20+
);
21+
this.OpenApiInfo = OpenApiInfo;
22+
}
23+
public IList<Model> GetOpenApiInfo()
24+
{
25+
return OpenApiInfo.Value;
26+
}
27+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace openapiinfo;
2+
public class Parameters
3+
{
4+
public string? Name { get; set; }
5+
public string? Location { get; set; }
6+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Text.Json;
2+
using Microsoft.OpenApi.Readers;
3+
4+
namespace openapiinfo;
5+
internal class Program
6+
{
7+
public static string openApiInfoFile = "openApiInfo.json";
8+
public static string openApiFileError = "openAPIErrors.csv";
9+
private const string openApiInfoMetadataUrl_v1 = "https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-powershell/metadata-changes-detection/openApiDocs/v1.0/OpenApiInfo/openApiInfo.json";
10+
private const string openApiInfoMetadataUrl_beta = "https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-powershell/metadata-changes-detection/openApiDocs/beta/OpenApiInfo/openApiInfo.json";
11+
private static IList<Model> openApiInfo_v1 = new OriginalMetadata(openApiInfoMetadataUrl_v1).GetOpenApiInfo();
12+
private static IList<Model> openApiInfo_beta = new OriginalMetadata(openApiInfoMetadataUrl_beta).GetOpenApiInfo();
13+
private static IDictionary<string, IList<Model>> openApiVersions = new Dictionary<string, IList<Model>>();
14+
private static void Main(string[] args)
15+
{
16+
openApiVersions.Add("v1.0", openApiInfo_v1);
17+
openApiVersions.Add("beta", openApiInfo_beta);
18+
19+
foreach (var version in openApiVersions)
20+
{
21+
CompareOpenApiInfo(version.Key, version.Value);
22+
}
23+
24+
}
25+
26+
private static void CompareOpenApiInfo(string version, IList<Model> openApiInfoMetadata)
27+
{
28+
List<Model> models = new List<Model>();
29+
var newPathsAdded = new HashSet<string>();
30+
var openApiErrors = new HashSet<string>();
31+
newPathsAdded.Add("Module,Path,Method");
32+
openApiErrors.Add("Module,ApiPath,Method,From,To");
33+
var filePath = FileHandler.GetOpenApiFolder();
34+
var combinedPath = filePath != null ? Path.Combine(filePath, version) : null;
35+
var openAPiInfoPath = combinedPath != null ? Path.Combine(combinedPath, "OpenAPiInfo") : null;
36+
var combinedErrorPath = openAPiInfoPath != null ? Path.Combine(openAPiInfoPath, openApiFileError) : null;
37+
if (combinedErrorPath != null && File.Exists(combinedErrorPath))
38+
{
39+
File.WriteAllText(combinedErrorPath, string.Empty);
40+
}
41+
if(combinedPath!=null)
42+
{
43+
//Go through list of openapi files
44+
foreach (var file in Directory.GetFiles(combinedPath))
45+
{
46+
using (var sr = new StreamReader(file))
47+
{
48+
var fileName = Path.GetFileNameWithoutExtension(file);
49+
var openApiDoc = new OpenApiStreamReader().Read(sr.BaseStream, out var diagnostic);
50+
if (diagnostic.Errors.Count > 0)
51+
{
52+
throw new Exception($"Error reading openapi file {file}");
53+
}
54+
//Go through each path in the openapi file
55+
foreach (var path in openApiDoc.Paths)
56+
{
57+
var model = new Model();
58+
var pathInfo = new PathInfo();
59+
//Get the path key
60+
var apiPath = path.Key;
61+
pathInfo.Path = apiPath;
62+
pathInfo.Module = fileName;
63+
model.PathInfo = pathInfo;
64+
//Go through each operation in the path
65+
foreach (var operation in path.Value.Operations)
66+
{
67+
var methodInfo = new MethodInfo();
68+
//Get the operationId
69+
var operationId = operation.Value.OperationId;
70+
methodInfo.OperationId = operationId;
71+
//Get the method
72+
var method = operation.Key.ToString();
73+
methodInfo.Method = method;
74+
//Get the parameters
75+
var parameters = new List<Parameters>();
76+
foreach (var parameter in operation.Value.Parameters)
77+
{
78+
var param = new Parameters();
79+
param.Name = parameter.Name;
80+
param.Location = parameter.In.ToString() ?? "NA";
81+
parameters.Add(param);
82+
}
83+
methodInfo.Parameters = parameters;
84+
model.MethodInfo = methodInfo;
85+
var originalPathDetails = PathDetails(openApiInfoMetadata, operationId, apiPath, method, fileName);
86+
if (originalPathDetails == null || originalPathDetails.PathInfo == null)
87+
{
88+
newPathsAdded.Add($"{fileName},{apiPath},{method}");
89+
continue;
90+
}
91+
if (originalPathDetails.MethodInfo != null && originalPathDetails.MethodInfo.Parameters != null && originalPathDetails.MethodInfo.Parameters.Count > methodInfo.Parameters.Count)
92+
{
93+
openApiErrors.Add($"{fileName},{apiPath}, {method},Parameter Count: {methodInfo.Parameters.Count}, Parameter Count: {originalPathDetails.MethodInfo.Parameters.Count}");
94+
}
95+
96+
if (originalPathDetails.MethodInfo != null && originalPathDetails.MethodInfo.OperationId != methodInfo.OperationId)
97+
{
98+
openApiErrors.Add($"{fileName},{apiPath}, {method},OperationId changed from: {originalPathDetails.MethodInfo.OperationId}, OperationId changed to: {methodInfo.OperationId}");
99+
}
100+
101+
102+
}
103+
models.Add(model);
104+
}
105+
}
106+
}
107+
}
108+
//convert list to json and add it to file
109+
if (newPathsAdded.Count > 1)
110+
{
111+
var options = new JsonSerializerOptions { WriteIndented = true };
112+
var json = JsonSerializer.Serialize(models, options);
113+
//Clear file first
114+
File.WriteAllText($"{openAPiInfoPath}\\{openApiInfoFile}", string.Empty);
115+
//then write to it with new content.
116+
File.WriteAllText($"{openAPiInfoPath}\\{openApiInfoFile}", json);
117+
}
118+
if (openApiErrors.Count > 1)
119+
{
120+
foreach (var error in openApiErrors)
121+
{
122+
var errList = error.Split(",");
123+
var report = $"{errList[0]},{errList[1]},{errList[2]},{errList[3]},{errList[4]}";
124+
File.AppendAllText($"{openAPiInfoPath}\\{openApiFileError}", report + Environment.NewLine);
125+
}
126+
}
127+
128+
}
129+
private static Model PathDetails(IList<Model> openApiInfo, string operationId, string apiPath, string method, string module)
130+
{
131+
if (openApiInfo == null || openApiInfo.Count == 0)
132+
{
133+
throw new Exception("OpenApiInfo is empty");
134+
}
135+
136+
var matchedPaths = openApiInfo.Where(c => c.MethodInfo != null && c.MethodInfo.Method == method
137+
&& c.PathInfo != null && c.PathInfo.Path == apiPath && c.PathInfo.Module == module).ToList();
138+
var pathDetails = matchedPaths.FirstOrDefault();
139+
140+
return pathDetails ?? new Model();
141+
}
142+
}

0 commit comments

Comments
 (0)