Skip to content

Commit 04af1a6

Browse files
committed
Make format optional; add logic for inspecting stream format
1 parent d022fa4 commit 04af1a6

File tree

9 files changed

+107
-34
lines changed

9 files changed

+107
-34
lines changed

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ private static string ConvertByteArrayToString(byte[] hash)
543543
/// <param name="settings">The OpenApi reader settings.</param>
544544
/// <returns></returns>
545545
public static ReadResult Load(MemoryStream stream,
546-
string format,
546+
string? format = null,
547547
OpenApiReaderSettings? settings = null)
548548
{
549549
return OpenApiModelFactory.Load(stream, format, settings);
@@ -568,7 +568,7 @@ public static async Task<ReadResult> LoadAsync(string url, OpenApiReaderSettings
568568
/// <param name="settings">The OpenApi reader settings.</param>
569569
/// <param name="cancellationToken">Propagates information about operation cancelling.</param>
570570
/// <returns></returns>
571-
public static async Task<ReadResult> LoadAsync(Stream stream, string format, OpenApiReaderSettings? settings = null, CancellationToken cancellationToken = default)
571+
public static async Task<ReadResult> LoadAsync(Stream stream, string? format = null, OpenApiReaderSettings? settings = null, CancellationToken cancellationToken = default)
572572
{
573573
return await OpenApiModelFactory.LoadAsync(stream, format, settings, cancellationToken);
574574
}

src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs

Lines changed: 84 additions & 16 deletions
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;
@@ -36,11 +36,13 @@ static OpenApiModelFactory()
3636
/// <param name="format">The OpenAPI format.</param>
3737
/// <returns>An OpenAPI document instance.</returns>
3838
public static ReadResult Load(MemoryStream stream,
39-
string format,
39+
string format = null,
4040
OpenApiReaderSettings settings = null)
4141
{
4242
settings ??= new OpenApiReaderSettings();
4343

44+
// Get the format of the stream if not provided
45+
format ??= InspectStreamFormat(stream);
4446
var result = InternalLoad(stream, format, settings);
4547

4648
if (!settings.LeaveStreamOpen)
@@ -61,7 +63,11 @@ public static ReadResult Load(MemoryStream stream,
6163
/// <param name="diagnostic"></param>
6264
/// <param name="settings"></param>
6365
/// <returns></returns>
64-
public static T Load<T>(Stream input, OpenApiSpecVersion version, string format, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
66+
public static T Load<T>(Stream input,
67+
OpenApiSpecVersion version,
68+
out OpenApiDiagnostic diagnostic,
69+
string format = null,
70+
OpenApiReaderSettings settings = null) where T : IOpenApiElement
6571
{
6672
if (input is MemoryStream memoryStream)
6773
{
@@ -89,7 +95,7 @@ public static T Load<T>(Stream input, OpenApiSpecVersion version, string format,
8995
/// <returns>The OpenAPI element.</returns>
9096
public static T Load<T>(MemoryStream input, OpenApiSpecVersion version, string format, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
9197
{
92-
format ??= OpenApiConstants.Json;
98+
format ??= InspectStreamFormat(input);
9399
return OpenApiReaderRegistry.GetReader(format).ReadFragment<T>(input, version, out diagnostic, settings);
94100
}
95101

@@ -117,7 +123,7 @@ public static async Task<ReadResult> LoadAsync(string url, OpenApiReaderSettings
117123
public static async Task<T> LoadAsync<T>(string url, OpenApiSpecVersion version, OpenApiReaderSettings settings = null) where T : IOpenApiElement
118124
{
119125
var result = await RetrieveStreamAndFormatAsync(url);
120-
return Load<T>(result.Item1, version, result.Item2, out var diagnostic, settings);
126+
return Load<T>(result.Item1, version, out var diagnostic, result.Item2, settings);
121127
}
122128

123129
/// <summary>
@@ -130,22 +136,17 @@ public static async Task<T> LoadAsync<T>(string url, OpenApiSpecVersion version,
130136
/// <returns></returns>
131137
public static async Task<ReadResult> LoadAsync(Stream input, string format = null, OpenApiReaderSettings settings = null, CancellationToken cancellationToken = default)
132138
{
133-
Utils.CheckArgumentNull(format, nameof(format));
134139
settings ??= new OpenApiReaderSettings();
135-
136140
Stream preparedStream;
137-
138-
// Avoid buffering for JSON documents
139-
if (input is MemoryStream || format.Equals(OpenApiConstants.Json, StringComparison.OrdinalIgnoreCase))
141+
if (format is null)
140142
{
141-
preparedStream = input;
143+
var readResult = await PrepareStreamForReadingAsync(input, format, cancellationToken);
144+
preparedStream = readResult.Item1;
145+
format = readResult.Item2;
142146
}
143147
else
144148
{
145-
// Buffer stream for non-JSON formats (e.g., YAML) since they require synchronous reading
146-
preparedStream = new MemoryStream();
147-
await input.CopyToAsync(preparedStream, 81920, cancellationToken);
148-
preparedStream.Position = 0;
149+
preparedStream = input;
149150
}
150151

151152
// Use StreamReader to process the prepared stream (buffered for YAML, direct for JSON)
@@ -232,7 +233,6 @@ private static async Task<OpenApiDiagnostic> LoadExternalRefsAsync(OpenApiDocume
232233

233234
private static ReadResult InternalLoad(MemoryStream input, string format, OpenApiReaderSettings settings)
234235
{
235-
Utils.CheckArgumentNull(format, nameof(format));
236236
if (settings.LoadExternalRefs)
237237
{
238238
throw new InvalidOperationException("Loading external references are not supported when using synchronous methods.");
@@ -293,5 +293,73 @@ private static string InspectInputFormat(string input)
293293
{
294294
return input.StartsWith("{", StringComparison.OrdinalIgnoreCase) || input.StartsWith("[", StringComparison.OrdinalIgnoreCase) ? OpenApiConstants.Json : OpenApiConstants.Yaml;
295295
}
296+
297+
private static string InspectStreamFormat(Stream stream)
298+
{
299+
if (stream == null) throw new ArgumentNullException(nameof(stream));
300+
301+
long initialPosition = stream.Position;
302+
int firstByte = stream.ReadByte();
303+
304+
// Skip whitespace if present and read the next non-whitespace byte
305+
if (char.IsWhiteSpace((char)firstByte))
306+
{
307+
firstByte = stream.ReadByte();
308+
}
309+
310+
stream.Position = initialPosition; // Reset the stream position to the beginning
311+
312+
char firstChar = (char)firstByte;
313+
return firstChar switch
314+
{
315+
'{' or '[' => OpenApiConstants.Json, // If the first character is '{' or '[', assume JSON
316+
_ => OpenApiConstants.Yaml // Otherwise assume YAML
317+
};
318+
}
319+
320+
private static async Task<(Stream, string)> PrepareStreamForReadingAsync(Stream input, string format, CancellationToken token = default)
321+
{
322+
Stream preparedStream = input;
323+
324+
if (!input.CanSeek)
325+
{
326+
// Use a temporary buffer to read a small portion for format detection
327+
using var bufferStream = new MemoryStream();
328+
await input.CopyToAsync(bufferStream, 1024, token);
329+
bufferStream.Position = 0;
330+
331+
// Inspect the format from the buffered portion
332+
format ??= InspectStreamFormat(bufferStream);
333+
334+
// If format is JSON, no need to buffer further — use the original stream.
335+
if (format.Equals(OpenApiConstants.Json, StringComparison.OrdinalIgnoreCase))
336+
{
337+
preparedStream = input;
338+
}
339+
else
340+
{
341+
// YAML or other non-JSON format; copy remaining input to a new stream.
342+
preparedStream = new MemoryStream();
343+
bufferStream.Position = 0;
344+
await bufferStream.CopyToAsync(preparedStream, 81920, token); // Copy buffered portion
345+
await input.CopyToAsync(preparedStream, 81920, token); // Copy remaining data
346+
preparedStream.Position = 0;
347+
}
348+
}
349+
else
350+
{
351+
format ??= InspectStreamFormat(input);
352+
353+
if (!format.Equals(OpenApiConstants.Json, StringComparison.OrdinalIgnoreCase))
354+
{
355+
// Buffer stream for non-JSON formats (e.g., YAML) since they require synchronous reading
356+
preparedStream = new MemoryStream();
357+
await input.CopyToAsync(preparedStream, 81920, token);
358+
preparedStream.Position = 0;
359+
}
360+
}
361+
362+
return (preparedStream, format);
363+
}
296364
}
297365
}

test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiStreamReaderTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public async Task StreamShouldCloseIfLeaveStreamOpenSettingEqualsFalse()
2525
{
2626
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStore.yaml"));
2727
var settings = new OpenApiReaderSettings { LeaveStreamOpen = false };
28-
_ = await OpenApiDocument.LoadAsync(stream, "yaml", settings);
28+
_ = await OpenApiDocument.LoadAsync(stream, settings: settings);
2929
Assert.False(stream.CanRead);
3030
}
3131

@@ -34,7 +34,7 @@ public async Task StreamShouldNotCloseIfLeaveStreamOpenSettingEqualsTrue()
3434
{
3535
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStore.yaml"));
3636
var settings = new OpenApiReaderSettings { LeaveStreamOpen = true };
37-
_ = await OpenApiDocument.LoadAsync(stream, "yaml", settings);
37+
_ = await OpenApiDocument.LoadAsync(stream, settings: settings);
3838
Assert.True(stream.CanRead);
3939
}
4040

@@ -48,7 +48,7 @@ public async Task StreamShouldNotBeDisposedIfLeaveStreamOpenSettingIsTrueAsync()
4848
memoryStream.Position = 0;
4949
var stream = memoryStream;
5050

51-
_ = await OpenApiDocument.LoadAsync(stream, "yaml", new OpenApiReaderSettings { LeaveStreamOpen = true });
51+
_ = await OpenApiDocument.LoadAsync(stream, settings: new OpenApiReaderSettings { LeaveStreamOpen = true });
5252
stream.Seek(0, SeekOrigin.Begin); // does not throw an object disposed exception
5353
Assert.True(stream.CanRead);
5454
}
@@ -64,7 +64,7 @@ public async Task StreamShouldReadWhenInitializedAsync()
6464
var stream = await httpClient.GetStreamAsync("20fe7a7b720a0e48e5842d002ac418b12a8201df/tests/v3.0/pass/petstore.yaml");
6565

6666
// Read V3 as YAML
67-
var result = await OpenApiDocument.LoadAsync(stream, "yaml");
67+
var result = await OpenApiDocument.LoadAsync(stream);
6868
Assert.NotNull(result.OpenApiDocument);
6969
}
7070
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ public async Task ParseBasicDocumentWithMultipleServersShouldSucceed()
156156
public async Task ParseBrokenMinimalDocumentShouldYieldExpectedDiagnostic()
157157
{
158158
using var stream = Resources.GetStream(System.IO.Path.Combine(SampleFolderPath, "brokenMinimalDocument.yaml"));
159-
var result = await OpenApiDocument.LoadAsync(stream, OpenApiConstants.Yaml);
159+
// Copy stream to MemoryStream
160+
using var memoryStream = new MemoryStream();
161+
await stream.CopyToAsync(memoryStream);
162+
memoryStream.Position = 0;
163+
164+
var result = OpenApiDocument.Load(memoryStream);
160165

161166
result.OpenApiDocument.Should().BeEquivalentTo(
162167
new OpenApiDocument

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void ParseAdvancedEncodingShouldSucceed()
4040
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "advancedEncoding.yaml"));
4141

4242
// Act
43-
var encoding = OpenApiModelFactory.Load<OpenApiEncoding>(stream, OpenApiSpecVersion.OpenApi3_0, OpenApiConstants.Yaml, out _);
43+
var encoding = OpenApiModelFactory.Load<OpenApiEncoding>(stream, OpenApiSpecVersion.OpenApi3_0, out _);
4444

4545
// Assert
4646
encoding.Should().BeEquivalentTo(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public void ParseMinimalInfoShouldSucceed()
114114
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "minimalInfo.yaml"));
115115

116116
// Act
117-
var openApiInfo = OpenApiModelFactory.Load<OpenApiInfo>(stream, OpenApiSpecVersion.OpenApi3_0, "yaml", out _);
117+
var openApiInfo = OpenApiModelFactory.Load<OpenApiInfo>(stream, OpenApiSpecVersion.OpenApi3_0, out _, "yaml");
118118

119119
// Assert
120120
openApiInfo.Should().BeEquivalentTo(

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void ParsePathParameterShouldSucceed()
3232
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "pathParameter.yaml"));
3333

3434
// Act
35-
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, "yaml", out _);
35+
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, out _, "yaml");
3636

3737
// Assert
3838
parameter.Should().BeEquivalentTo(
@@ -107,7 +107,7 @@ public void ParseQueryParameterWithObjectTypeAndContentShouldSucceed()
107107
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "queryParameterWithObjectTypeAndContent.yaml"));
108108

109109
// Act
110-
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, "yaml", out _);
110+
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, out _, "yaml");
111111

112112
// Assert
113113
parameter.Should().BeEquivalentTo(
@@ -200,7 +200,7 @@ public void ParseParameterWithNoLocationShouldSucceed()
200200
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithNoLocation.yaml"));
201201

202202
// Act
203-
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, "yaml", out _);
203+
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, out _);
204204

205205
// Assert
206206
parameter.Should().BeEquivalentTo(
@@ -224,7 +224,7 @@ public void ParseParameterWithUnknownLocationShouldSucceed()
224224
using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithUnknownLocation.yaml"));
225225

226226
// Act
227-
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, "yaml", out _);
227+
var parameter = OpenApiModelFactory.Load<OpenApiParameter>(stream, OpenApiSpecVersion.OpenApi3_0, out _);
228228

229229
// Assert
230230
parameter.Should().BeEquivalentTo(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public OpenApiXmlTests()
2424
public void ParseBasicXmlShouldSucceed()
2525
{
2626
// Act
27-
var xml = OpenApiModelFactory.Load<OpenApiXml>(Resources.GetStream(Path.Combine(SampleFolderPath, "basicXml.yaml")), OpenApiSpecVersion.OpenApi3_0, "yaml", out _);
27+
var xml = OpenApiModelFactory.Load<OpenApiXml>(Resources.GetStream(Path.Combine(SampleFolderPath, "basicXml.yaml")), OpenApiSpecVersion.OpenApi3_0, out _);
2828

2929
// Assert
3030
xml.Should().BeEquivalentTo(

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,9 +578,9 @@ namespace Microsoft.OpenApi.Models
578578
public void SerializeAsV31(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { }
579579
public void SetReferenceHostDocument() { }
580580
public static string GenerateHashValue(Microsoft.OpenApi.Models.OpenApiDocument doc) { }
581-
public static Microsoft.OpenApi.Reader.ReadResult Load(System.IO.MemoryStream stream, string format, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null) { }
581+
public static Microsoft.OpenApi.Reader.ReadResult Load(System.IO.MemoryStream stream, string? format = null, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null) { }
582582
public static System.Threading.Tasks.Task<Microsoft.OpenApi.Reader.ReadResult> LoadAsync(string url, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null) { }
583-
public static System.Threading.Tasks.Task<Microsoft.OpenApi.Reader.ReadResult> LoadAsync(System.IO.Stream stream, string format, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null, System.Threading.CancellationToken cancellationToken = default) { }
583+
public static System.Threading.Tasks.Task<Microsoft.OpenApi.Reader.ReadResult> LoadAsync(System.IO.Stream stream, string? format = null, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null, System.Threading.CancellationToken cancellationToken = default) { }
584584
public static Microsoft.OpenApi.Reader.ReadResult Parse(string input, string? format = null, Microsoft.OpenApi.Reader.OpenApiReaderSettings? settings = null) { }
585585
}
586586
public class OpenApiEncoding : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiExtensible, Microsoft.OpenApi.Interfaces.IOpenApiSerializable
@@ -1323,10 +1323,10 @@ namespace Microsoft.OpenApi.Reader
13231323
}
13241324
public static class OpenApiModelFactory
13251325
{
1326-
public static Microsoft.OpenApi.Reader.ReadResult Load(System.IO.MemoryStream stream, string format, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null) { }
1326+
public static Microsoft.OpenApi.Reader.ReadResult Load(System.IO.MemoryStream stream, string format = null, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null) { }
13271327
public static T Load<T>(System.IO.MemoryStream input, Microsoft.OpenApi.OpenApiSpecVersion version, string format, out Microsoft.OpenApi.Reader.OpenApiDiagnostic diagnostic, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null)
13281328
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
1329-
public static T Load<T>(System.IO.Stream input, Microsoft.OpenApi.OpenApiSpecVersion version, string format, out Microsoft.OpenApi.Reader.OpenApiDiagnostic diagnostic, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null)
1329+
public static T Load<T>(System.IO.Stream input, Microsoft.OpenApi.OpenApiSpecVersion version, out Microsoft.OpenApi.Reader.OpenApiDiagnostic diagnostic, string format = null, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null)
13301330
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
13311331
public static System.Threading.Tasks.Task<Microsoft.OpenApi.Reader.ReadResult> LoadAsync(string url, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null) { }
13321332
public static System.Threading.Tasks.Task<Microsoft.OpenApi.Reader.ReadResult> LoadAsync(System.IO.Stream input, string format = null, Microsoft.OpenApi.Reader.OpenApiReaderSettings settings = null, System.Threading.CancellationToken cancellationToken = default) { }

0 commit comments

Comments
 (0)