Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
56bb532
feat: enable NRT
MaggieKimani1 Feb 13, 2025
b3b5663
fix: dereference of a possible null reference
MaggieKimani1 Feb 13, 2025
c99dfc3
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Feb 13, 2025
489fa4d
chore: code cleanup
MaggieKimani1 Feb 14, 2025
0b6b07d
chore: convert to conditional expression
MaggieKimani1 Feb 14, 2025
f13a9e4
Update src/Microsoft.OpenApi/Models/OpenApiOperation.cs
MaggieKimani1 Feb 17, 2025
b7b845c
Update src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs
MaggieKimani1 Feb 21, 2025
ea5c389
chore: PR feedback
MaggieKimani1 Feb 21, 2025
78b8d88
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Feb 21, 2025
1b793a4
fix: resolve merge conflict errors
MaggieKimani1 Feb 24, 2025
3df5c98
chore: remove deprecated code and tests; make param required
MaggieKimani1 Feb 24, 2025
69a2b9b
chore: code refactor and cleanup
MaggieKimani1 Feb 24, 2025
2133509
chore: update public API
MaggieKimani1 Feb 24, 2025
d990121
chore: address PR comments
MaggieKimani1 Feb 25, 2025
80edd66
fix: remove nullable Json node params
MaggieKimani1 Feb 25, 2025
2749a3e
chore: use conditional compilation to make reference a required field
MaggieKimani1 Feb 27, 2025
fa012ac
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Feb 27, 2025
d016afd
chore: resolve merge conflicts
MaggieKimani1 Feb 27, 2025
fd15b0a
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Feb 27, 2025
9ef1bb0
chore: resolve merge conflicts
MaggieKimani1 Feb 27, 2025
07b3a2c
refactor: apply nullable to new changes
MaggieKimani1 Feb 27, 2025
cd41026
Merge branch 'main' into feat/enable-NRT
baywet Feb 27, 2025
ad59b9c
chore: bad merge
baywet Feb 27, 2025
b1b2fc5
Merge branch 'main' into feat/enable-NRT
baywet Mar 3, 2025
b6d68bb
chore: simplifies filtering condition
baywet Mar 3, 2025
e6cc29c
Update src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs
MaggieKimani1 Mar 4, 2025
8a4fcc2
chore: address more PR feedback
MaggieKimani1 Mar 4, 2025
6116afe
chore: pull latest changes
MaggieKimani1 Mar 4, 2025
0805d89
chore: add check for both null and empty strings
MaggieKimani1 Mar 4, 2025
0a690ff
chore: revert to include check for empty strings
MaggieKimani1 Mar 4, 2025
af8a41c
chore: cleanup
MaggieKimani1 Mar 4, 2025
d9afa53
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Mar 4, 2025
cd4de69
chore: clean up public API
MaggieKimani1 Mar 4, 2025
0176234
Update src/Microsoft.OpenApi/Reader/Services/OpenApiRemoteReferenceCo…
MaggieKimani1 Mar 10, 2025
2e0b42d
fix: resolve PR feedback
MaggieKimani1 Mar 11, 2025
7f71fb7
fix: apply suggestion
MaggieKimani1 Mar 11, 2025
7a9b01e
chore: Merge remote-tracking branch 'origin/main' into feat/enable-NRT
MaggieKimani1 Mar 12, 2025
c5caa27
chore: add defensive programming
MaggieKimani1 Mar 12, 2025
02c95e8
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Mar 12, 2025
ff0d877
chore: resolve merge conflicts
MaggieKimani1 Mar 12, 2025
2dcc72e
chore: more refactoring
MaggieKimani1 Mar 12, 2025
0aea73f
Update src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs
MaggieKimani1 Mar 12, 2025
f942764
Update src/Microsoft.OpenApi/Reader/V2/OpenApiPathItemDeserializer.cs
MaggieKimani1 Mar 12, 2025
e545160
Update src/Microsoft.OpenApi/Reader/V31/OpenApiServerVariableDeserial…
MaggieKimani1 Mar 12, 2025
6cc7497
Update src/Microsoft.OpenApi/Reader/V31/OpenApiSecurityRequirementDes…
MaggieKimani1 Mar 12, 2025
93ac7f0
Update src/Microsoft.OpenApi/Reader/V2/OpenApiSecurityRequirementDese…
MaggieKimani1 Mar 12, 2025
571b537
Update src/Microsoft.OpenApi/Reader/V3/OpenApiServerVariableDeseriali…
MaggieKimani1 Mar 12, 2025
69ef5c0
Update src/Microsoft.OpenApi/Reader/V3/OpenApiSecurityRequirementDese…
MaggieKimani1 Mar 12, 2025
81e0682
chore: another round of refactoring
MaggieKimani1 Mar 12, 2025
d9a7bec
chore: clean up nullability of params
MaggieKimani1 Mar 13, 2025
32568e8
fix: compiler errors
MaggieKimani1 Mar 13, 2025
f608cea
fix: remove redundant cast and update public API
MaggieKimani1 Mar 13, 2025
2f8ea28
Merge branch 'main' into feat/enable-NRT
MaggieKimani1 Mar 13, 2025
50b06c4
chore: fix merge conflict issues
MaggieKimani1 Mar 13, 2025
c9b8f4f
chore: apply copilot suggestion
MaggieKimani1 Mar 13, 2025
2a12fa7
Update src/Microsoft.OpenApi/Services/OpenApiWalker.cs
baywet Mar 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override void Visit(IOpenApiSchema schema)

public override void Visit(IOpenApiPathItem pathItem)
{
if (pathItem.Operations.TryGetValue(OperationType.Put, out var value) &&
if (pathItem.Operations is not null && pathItem.Operations.TryGetValue(OperationType.Put, out var value) &&
value.OperationId != null)
{
var operationId = value.OperationId;
Expand Down Expand Up @@ -149,7 +149,7 @@ private static string RemoveKeyTypeSegment(string operationId, IList<IOpenApiPar
var segments = operationId.SplitByChar('.');
foreach (var parameter in parameters)
{
var keyTypeExtension = parameter.Extensions.GetExtension("x-ms-docs-key-type");
var keyTypeExtension = parameter.Extensions?.GetExtension("x-ms-docs-key-type");
if (keyTypeExtension != null && operationId.Contains(keyTypeExtension, StringComparison.OrdinalIgnoreCase))
{
segments.Remove(keyTypeExtension);
Expand Down Expand Up @@ -178,15 +178,20 @@ private static void ResolveFunctionParameters(IList<IOpenApiParameter> parameter

private void AddAdditionalPropertiesToSchema(IOpenApiSchema schema)
{
if (schema is OpenApiSchema openApiSchema && !_schemaLoop.Contains(schema) && schema.Type.Equals(JsonSchemaType.Object))
if (schema is OpenApiSchema openApiSchema
&& !_schemaLoop.Contains(schema)
&& schema.Type.Equals(JsonSchemaType.Object))
{
openApiSchema.AdditionalProperties = new OpenApiSchema() { Type = JsonSchemaType.Object };

/* Because 'additionalProperties' are now being walked,
* we need a way to keep track of visited schemas to avoid
* endlessly creating and walking them in an infinite recursion.
*/
_schemaLoop.Push(schema.AdditionalProperties);
if (schema.AdditionalProperties is not null)
{
_schemaLoop.Push(schema.AdditionalProperties);
}
}
}

Expand Down
146 changes: 80 additions & 66 deletions src/Microsoft.OpenApi.Hidi/OpenApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static async Task TransformOpenApiDocumentAsync(HidiOptions options, ILog
// Load OpenAPI document
var document = await GetOpenApiAsync(options, openApiFormat.GetDisplayName(), logger, options.MetadataVersion, cancellationToken).ConfigureAwait(false);

if (options.FilterOptions != null)
if (options.FilterOptions != null && document is not null)
{
document = ApplyFilters(options, logger, apiDependency, postmanCollection, document);
}
Expand All @@ -107,7 +107,11 @@ public static async Task TransformOpenApiDocumentAsync(HidiOptions options, ILog
var walker = new OpenApiWalker(powerShellFormatter);
walker.Walk(document);
}
await WriteOpenApiAsync(options, openApiFormat, openApiVersion, document, logger, cancellationToken).ConfigureAwait(false);
if (document is not null)
{
// Write the OpenAPI document to the output file
await WriteOpenApiAsync(options, openApiFormat, openApiVersion, document, logger, cancellationToken).ConfigureAwait(false);
}
}
catch (TaskCanceledException)
{
Expand Down Expand Up @@ -172,7 +176,7 @@ private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger,
options.FilterOptions.FilterByTags,
requestUrls,
document,
logger);
logger);
if (predicate != null)
{
var stopwatch = new Stopwatch();
Expand Down Expand Up @@ -210,6 +214,7 @@ private static async Task WriteOpenApiAsync(HidiOptions options, OpenApiFormat o

var stopwatch = new Stopwatch();
stopwatch.Start();

await document.SerializeAsync(writer, openApiVersion, cancellationToken).ConfigureAwait(false);
stopwatch.Stop();

Expand All @@ -219,9 +224,9 @@ private static async Task WriteOpenApiAsync(HidiOptions options, OpenApiFormat o
}

// Get OpenAPI document either from OpenAPI or CSDL
private static async Task<OpenApiDocument> GetOpenApiAsync(HidiOptions options, string format, ILogger logger, string? metadataVersion = null, CancellationToken cancellationToken = default)
private static async Task<OpenApiDocument?> GetOpenApiAsync(HidiOptions options, string format, ILogger logger, string? metadataVersion = null, CancellationToken cancellationToken = default)
{
OpenApiDocument document;
OpenApiDocument? document;
Stream stream;

if (!string.IsNullOrEmpty(options.Csdl))
Expand All @@ -242,7 +247,7 @@ private static async Task<OpenApiDocument> GetOpenApiAsync(HidiOptions options,

document = await ConvertCsdlToOpenApiAsync(filteredStream ?? stream, format, metadataVersion, options.SettingsConfig, cancellationToken).ConfigureAwait(false);
stopwatch.Stop();
logger.LogTrace("{Timestamp}ms: Generated OpenAPI with {Paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count);
logger.LogTrace("{Timestamp}ms: Generated OpenAPI with {Paths} paths.", stopwatch.ElapsedMilliseconds, document?.Paths.Count);
}
}
else if (!string.IsNullOrEmpty(options.OpenApi))
Expand Down Expand Up @@ -370,7 +375,7 @@ private static MemoryStream ApplyFilterToCsdl(Stream csdlStream, string entitySe

if (result is null) return null;

return result.Diagnostic.Errors.Count == 0;
return result.Diagnostic?.Errors.Count == 0;
}

private static async Task<ReadResult> ParseOpenApiAsync(string openApiFile, bool inlineExternal, ILogger logger, Stream stream, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -407,7 +412,7 @@ private static async Task<ReadResult> ParseOpenApiAsync(string openApiFile, bool
/// </summary>
/// <param name="csdl">The CSDL stream.</param>
/// <returns>An OpenAPI document.</returns>
public static async Task<OpenApiDocument> ConvertCsdlToOpenApiAsync(Stream csdl, string format, string? metadataVersion = null, IConfiguration? settings = null, CancellationToken token = default)
public static async Task<OpenApiDocument?> ConvertCsdlToOpenApiAsync(Stream csdl, string format, string? metadataVersion = null, IConfiguration? settings = null, CancellationToken token = default)
{
using var reader = new StreamReader(csdl);
var csdlText = await reader.ReadToEndAsync(token).ConfigureAwait(false);
Expand All @@ -425,7 +430,7 @@ public static async Task<OpenApiDocument> ConvertCsdlToOpenApiAsync(Stream csdl,
/// </summary>
/// <param name="document"> The converted OpenApiDocument.</param>
/// <returns> A valid OpenApiDocument instance.</returns>
public static OpenApiDocument FixReferences(OpenApiDocument document, string format)
public static OpenApiDocument? FixReferences(OpenApiDocument document, string format)
{
// 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.
Expand Down Expand Up @@ -584,52 +589,54 @@ private static string GetInputPathExtension(string? openapi = null, string? csdl

var openApiFormat = options.OpenApiFormat ?? (!string.IsNullOrEmpty(options.OpenApi) ? GetOpenApiFormat(options.OpenApi, logger) : OpenApiFormat.Yaml);
var document = await GetOpenApiAsync(options, openApiFormat.GetDisplayName(), logger, null, cancellationToken).ConfigureAwait(false);

using (logger.BeginScope("Creating diagram"))
if (document is not null)
{
// If output is null, create a HTML file in the user's temporary directory
var sourceUrl = (string.IsNullOrEmpty(options.OpenApi), string.IsNullOrEmpty(options.Csdl)) switch {
(false, _) => options.OpenApi!,
(_, false) => options.Csdl!,
_ => throw new InvalidOperationException("No input file path or URL provided")
};
if (options.Output == null)
using (logger.BeginScope("Creating diagram"))
{
var tempPath = Path.GetTempPath() + "/hidi/";
if (!File.Exists(tempPath))
// If output is null, create a HTML file in the user's temporary directory
var sourceUrl = (string.IsNullOrEmpty(options.OpenApi), string.IsNullOrEmpty(options.Csdl)) switch
{
Directory.CreateDirectory(tempPath);
}

var fileName = Path.GetRandomFileName();

var output = new FileInfo(Path.Combine(tempPath, fileName + ".html"));
using (var file = new FileStream(output.FullName, FileMode.Create))
(false, _) => options.OpenApi!,
(_, false) => options.Csdl!,
_ => throw new InvalidOperationException("No input file path or URL provided")
};
if (options.Output == null)
{
using var writer = new StreamWriter(file);
WriteTreeDocumentAsHtml(sourceUrl, document, writer);
var tempPath = Path.GetTempPath() + "/hidi/";
if (!File.Exists(tempPath))
{
Directory.CreateDirectory(tempPath);
}

var fileName = Path.GetRandomFileName();

var output = new FileInfo(Path.Combine(tempPath, fileName + ".html"));
using (var file = new FileStream(output.FullName, FileMode.Create))
{
using var writer = new StreamWriter(file);
WriteTreeDocumentAsHtml(sourceUrl, document, writer);
}
logger.LogTrace("Created Html document with diagram ");

// Launch a browser to display the output html file
using var process = new Process();
process.StartInfo.FileName = output.FullName;
process.StartInfo.UseShellExecute = true;
process.Start();

return output.FullName;
}
logger.LogTrace("Created Html document with diagram ");

// Launch a browser to display the output html file
using var process = new Process();
process.StartInfo.FileName = output.FullName;
process.StartInfo.UseShellExecute = true;
process.Start();

return output.FullName;
}
else // Write diagram as Markdown document to output file
{
using (var file = new FileStream(options.Output.FullName, FileMode.Create))
else // Write diagram as Markdown document to output file
{
using var file = new FileStream(options.Output.FullName, FileMode.Create);
using var writer = new StreamWriter(file);
WriteTreeDocumentAsMarkdown(sourceUrl, document, writer);

logger.LogTrace("Created markdown document with diagram ");
return options.Output.FullName;
}
logger.LogTrace("Created markdown document with diagram ");
return options.Output.FullName;
}
}
}
}
catch (TaskCanceledException)
{
Expand All @@ -645,7 +652,7 @@ private static string GetInputPathExtension(string? openapi = null, string? csdl
private static void LogErrors(ILogger logger, ReadResult result)
{
var context = result.Diagnostic;
if (context.Errors.Count != 0)
if (context is not null && context.Errors.Count != 0)
{
using (logger.BeginScope("Detected errors"))
{
Expand Down Expand Up @@ -697,7 +704,7 @@ internal static void WriteTreeDocumentAsHtml(string sourceUrl, OpenApiDocument d
</style>
<body>
""");
writer.WriteLine("<h1>" + document.Info.Title + "</h1>");
writer.WriteLine("<h1>" + document?.Info.Title + "</h1>");
writer.WriteLine();
writer.WriteLine($"<h3> API Description: <a href='{sourceUrl}'>{sourceUrl}</a></h3>");

Expand Down Expand Up @@ -751,7 +758,7 @@ internal static async Task PluginManifestAsync(HidiOptions options, ILogger logg

cancellationToken.ThrowIfCancellationRequested();

if (options.FilterOptions != null)
if (options.FilterOptions != null && document is not null)
{
document = ApplyFilters(options, logger, apiDependency, null, document);
}
Expand All @@ -765,24 +772,31 @@ internal static async Task PluginManifestAsync(HidiOptions options, ILogger logg
// Write OpenAPI to Output folder
options.Output = new(Path.Combine(options.OutputFolder, "openapi.json"));
options.TerseOutput = true;
await WriteOpenApiAsync(options, OpenApiFormat.Json, OpenApiSpecVersion.OpenApi3_1, document, logger, cancellationToken).ConfigureAwait(false);

// Create OpenAIPluginManifest from ApiDependency and OpenAPI document
var manifest = new OpenAIPluginManifest(document.Info?.Title ?? "Title", document.Info?.Title ?? "Title", "https://go.microsoft.com/fwlink/?LinkID=288890", document.Info?.Contact?.Email ?? "[email protected]", document.Info?.License?.Url.ToString() ?? "https://placeholderlicenseurl.com")
{
DescriptionForHuman = document.Info?.Description ?? "Description placeholder",
Api = new("openapi", "./openapi.json"),
Auth = new ManifestNoAuth(),
};
manifest.NameForModel = manifest.NameForHuman;
manifest.DescriptionForModel = manifest.DescriptionForHuman;

// Write OpenAIPluginManifest to Output folder
var manifestFile = new FileInfo(Path.Combine(options.OutputFolder, "ai-plugin.json"));
using var file = new FileStream(manifestFile.FullName, FileMode.Create);
using var jsonWriter = new Utf8JsonWriter(file, new() { Indented = true });
manifest.Write(jsonWriter);
await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
if (document is not null)
{
await WriteOpenApiAsync(options, OpenApiFormat.Json, OpenApiSpecVersion.OpenApi3_1, document, logger, cancellationToken).ConfigureAwait(false);

// Create OpenAIPluginManifest from ApiDependency and OpenAPI document
var manifest = new OpenAIPluginManifest(document.Info.Title ?? "Title",
document.Info.Title ?? "Title",
"https://go.microsoft.com/fwlink/?LinkID=288890",
document.Info?.Contact?.Email ?? "[email protected]",
document.Info?.License?.Url?.ToString() ?? "https://placeholderlicenseurl.com")
{
DescriptionForHuman = document.Info?.Description ?? "Description placeholder",
Api = new("openapi", "./openapi.json"),
Auth = new ManifestNoAuth(),
};
manifest.NameForModel = manifest.NameForHuman;
manifest.DescriptionForModel = manifest.DescriptionForHuman;

// Write OpenAIPluginManifest to Output folder
var manifestFile = new FileInfo(Path.Combine(options.OutputFolder, "ai-plugin.json"));
using var file = new FileStream(manifestFile.FullName, FileMode.Create);
using var jsonWriter = new Utf8JsonWriter(file, new() { Indented = true });
manifest.Write(jsonWriter);
await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#embeduntrackedsources -->
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<NoWarn>NU5048</NoWarn>
<Nullable>enable</Nullable>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

Expand Down
6 changes: 3 additions & 3 deletions src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ public static ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings)
}

/// <inheritdoc/>
public T ReadFragment<T>(MemoryStream input,
public T? ReadFragment<T>(MemoryStream input,
OpenApiSpecVersion version,
OpenApiDocument openApiDocument,
out OpenApiDiagnostic diagnostic,
OpenApiReaderSettings settings = null) where T : IOpenApiElement
OpenApiReaderSettings? settings = null) where T : IOpenApiElement
{
if (input is null) throw new ArgumentNullException(nameof(input));
JsonNode jsonNode;
Expand All @@ -110,7 +110,7 @@ public T ReadFragment<T>(MemoryStream input,
}

/// <inheritdoc/>
public static T ReadFragment<T>(JsonNode input, OpenApiSpecVersion version, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
public static T? ReadFragment<T>(JsonNode input, OpenApiSpecVersion version, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings? settings = null) where T : IOpenApiElement
{
return _jsonReader.ReadFragment<T>(input, version, openApiDocument, out diagnostic, settings);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Exceptions/OpenApiException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public OpenApiException(string message)
/// </summary>
/// <param name="message">The plain text error message for this exception.</param>
/// <param name="innerException">The inner exception that is the cause of this exception to be thrown.</param>
public OpenApiException(string message, Exception innerException)
public OpenApiException(string message, Exception? innerException)
: base(message, innerException)
{
}
Expand All @@ -46,6 +46,6 @@ public OpenApiException(string message, Exception innerException)
/// a text/plain pointer as defined in https://tools.ietf.org/html/rfc5147
/// Currently only line= is provided because using char= causes tests to break due to CR/LF and LF differences
/// </summary>
public string Pointer { get; set; }
public string? Pointer { get; set; }
}
}
Loading