Skip to content

Commit 79c271e

Browse files
authored
Merge pull request #619 from /issues/603
Fix resolution of external files
2 parents 33a573b + 56c07ec commit 79c271e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1092
-316
lines changed

src/Microsoft.OpenApi.Hidi/OpenApiService.cs

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
using System.Net.Http;
1111
using System.Security;
1212
using System.Text;
13-
using System.Text.Json;
1413
using System.Threading.Tasks;
14+
using System.Text.Json;
1515
using Microsoft.Extensions.Logging;
1616
using System.Xml.Linq;
1717
using Microsoft.OData.Edm.Csdl;
@@ -40,8 +40,8 @@ public static async Task<int> TransformOpenApiDocument(
4040
string? version,
4141
OpenApiFormat? format,
4242
LogLevel loglevel,
43-
bool inline,
44-
bool resolveexternal,
43+
bool inlineLocal,
44+
bool inlineExternal,
4545
string filterbyoperationids,
4646
string filterbytags,
4747
string filterbycollection,
@@ -99,8 +99,9 @@ CancellationToken cancellationToken
9999
stopwatch.Restart();
100100
var result = await new OpenApiStreamReader(new OpenApiReaderSettings
101101
{
102-
ReferenceResolution = resolveexternal ? ReferenceResolutionSetting.ResolveAllReferences : ReferenceResolutionSetting.ResolveLocalReferences,
103-
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
102+
RuleSet = ValidationRuleSet.GetDefaultRuleSet(),
103+
LoadExternalRefs = inlineExternal,
104+
BaseUrl = openapi.StartsWith("http") ? new Uri(openapi) : new Uri("file:" + new FileInfo(openapi).DirectoryName + "\\")
104105
}
105106
).ReadAsync(stream);
106107

@@ -177,7 +178,8 @@ CancellationToken cancellationToken
177178

178179
var settings = new OpenApiWriterSettings()
179180
{
180-
ReferenceInline = inline ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences
181+
InlineLocalReferences = inlineLocal,
182+
InlineExternalReferences = inlineExternal
181183
};
182184

183185
IOpenApiWriter writer = openApiFormat switch
@@ -239,7 +241,7 @@ public static async Task<int> ValidateOpenApiDocument(
239241
RuleSet = ValidationRuleSet.GetDefaultRuleSet()
240242
}
241243
).ReadAsync(stream);
242-
244+
243245
logger.LogTrace("{timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds);
244246

245247
document = result.OpenApiDocument;
@@ -316,6 +318,73 @@ public static async Task<OpenApiDocument> ConvertCsdlToOpenApi(Stream csdl)
316318
return document;
317319
}
318320

321+
/// <summary>
322+
/// Fixes the references in the resulting OpenApiDocument.
323+
/// </summary>
324+
/// <param name="document"> The converted OpenApiDocument.</param>
325+
/// <returns> A valid OpenApiDocument instance.</returns>
326+
public static OpenApiDocument FixReferences(OpenApiDocument document)
327+
{
328+
// This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance.
329+
// So we write it out, and read it back in again to fix it up.
330+
331+
var sb = new StringBuilder();
332+
document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb)));
333+
var doc = new OpenApiStringReader().Read(sb.ToString(), out _);
334+
335+
return doc;
336+
}
337+
338+
private static async Task<Stream> GetStream(string input, ILogger logger)
339+
{
340+
var stopwatch = new Stopwatch();
341+
stopwatch.Start();
342+
343+
Stream stream;
344+
if (input.StartsWith("http"))
345+
{
346+
try
347+
{
348+
var httpClientHandler = new HttpClientHandler()
349+
{
350+
SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
351+
};
352+
using var httpClient = new HttpClient(httpClientHandler)
353+
{
354+
DefaultRequestVersion = HttpVersion.Version20
355+
};
356+
stream = await httpClient.GetStreamAsync(input);
357+
}
358+
catch (HttpRequestException ex)
359+
{
360+
logger.LogError($"Could not download the file at {input}, reason{ex}");
361+
return null;
362+
}
363+
}
364+
else
365+
{
366+
try
367+
{
368+
var fileInput = new FileInfo(input);
369+
stream = fileInput.OpenRead();
370+
}
371+
catch (Exception ex) when (ex is FileNotFoundException ||
372+
ex is PathTooLongException ||
373+
ex is DirectoryNotFoundException ||
374+
ex is IOException ||
375+
ex is UnauthorizedAccessException ||
376+
ex is SecurityException ||
377+
ex is NotSupportedException)
378+
{
379+
logger.LogError($"Could not open the file at {input}, reason: {ex.Message}");
380+
return null;
381+
}
382+
}
383+
stopwatch.Stop();
384+
logger.LogTrace("{timestamp}ms: Read file {input}", stopwatch.ElapsedMilliseconds, input);
385+
return stream;
386+
}
387+
319388
/// <summary>
320389
/// Takes in a file stream, parses the stream into a JsonDocument and gets a list of paths and Http methods
321390
/// </summary>
@@ -353,17 +422,28 @@ public static Dictionary<string, List<string>> ParseJsonCollectionFile(Stream st
353422
/// </summary>
354423
/// <param name="document"> The converted OpenApiDocument.</param>
355424
/// <returns> A valid OpenApiDocument instance.</returns>
356-
private static OpenApiDocument FixReferences(OpenApiDocument document)
357-
{
358-
// This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance.
359-
// So we write it out, and read it back in again to fix it up.
360-
361-
var sb = new StringBuilder();
362-
document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb)));
363-
var doc = new OpenApiStringReader().Read(sb.ToString(), out _);
364-
365-
return doc;
366-
}
425+
// private static OpenApiDocument FixReferences2(OpenApiDocument document)
426+
// {
427+
// // This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance.
428+
// // So we write it out, and read it back in again to fix it up.
429+
430+
// OpenApiDocument document;
431+
// logger.LogTrace("Parsing the OpenApi file");
432+
// var result = await new OpenApiStreamReader(new OpenApiReaderSettings
433+
// {
434+
// RuleSet = ValidationRuleSet.GetDefaultRuleSet(),
435+
// BaseUrl = new Uri(openapi)
436+
// }
437+
// ).ReadAsync(stream);
438+
439+
// document = result.OpenApiDocument;
440+
// var context = result.OpenApiDiagnostic;
441+
// var sb = new StringBuilder();
442+
// document.SerializeAsV3(new OpenApiYamlWriter(new StringWriter(sb)));
443+
// var doc = new OpenApiStringReader().Read(sb.ToString(), out _);
444+
445+
// return doc;
446+
// }
367447

368448
/// <summary>
369449
/// Reads stream from file system or makes HTTP request depending on the input string

src/Microsoft.OpenApi.Hidi/Program.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ static async Task Main(string[] args)
4848
var filterByCollectionOption = new Option<string>("--filter-by-collection", "Filters OpenApiDocument by Postman collection provided");
4949
filterByCollectionOption.AddAlias("-c");
5050

51-
var inlineOption = new Option<bool>("--inline", "Inline $ref instances");
52-
inlineOption.AddAlias("-i");
51+
var inlineLocalOption = new Option<bool>("--inlineLocal", "Inline local $ref instances");
52+
inlineLocalOption.AddAlias("-il");
5353

54-
var resolveExternalOption = new Option<bool>("--resolve-external", "Resolve external $refs");
55-
resolveExternalOption.AddAlias("-ex");
54+
var inlineExternalOption = new Option<bool>("--inlineExternal", "Inline external $ref instances");
55+
inlineExternalOption.AddAlias("-ie");
5656

5757
var validateCommand = new Command("validate")
5858
{
@@ -74,12 +74,12 @@ static async Task Main(string[] args)
7474
filterByOperationIdsOption,
7575
filterByTagsOption,
7676
filterByCollectionOption,
77-
inlineOption,
78-
resolveExternalOption,
77+
inlineLocalOption,
78+
inlineExternalOption
7979
};
8080

8181
transformCommand.SetHandler<string, string, FileInfo, bool, string?, OpenApiFormat?, LogLevel, bool, bool, string, string, string, CancellationToken> (
82-
OpenApiService.TransformOpenApiDocument, descriptionOption, csdlOption, outputOption, cleanOutputOption, versionOption, formatOption, logLevelOption, inlineOption, resolveExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption);
82+
OpenApiService.TransformOpenApiDocument, descriptionOption, csdlOption, outputOption, cleanOutputOption, versionOption, formatOption, logLevelOption, inlineLocalOption, inlineExternalOption, filterByOperationIdsOption, filterByTagsOption, filterByCollectionOption);
8383

8484
rootCommand.Add(transformCommand);
8585
rootCommand.Add(validateCommand);

src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@
44
using Microsoft.OpenApi.Any;
55
using Microsoft.OpenApi.Interfaces;
66
using Microsoft.OpenApi.Readers.Interface;
7-
using Microsoft.OpenApi.Readers.ParseNodes;
8-
using Microsoft.OpenApi.Readers.Services;
97
using Microsoft.OpenApi.Validations;
108
using System;
119
using System.Collections.Generic;
1210
using System.IO;
13-
using System.Linq;
14-
using System.Text;
15-
using System.Threading.Tasks;
1611

1712
namespace Microsoft.OpenApi.Readers
1813
{
@@ -30,7 +25,7 @@ public enum ReferenceResolutionSetting
3025
/// </summary>
3126
ResolveLocalReferences,
3227
/// <summary>
33-
/// Convert all references to references of valid domain objects.
28+
/// ResolveAllReferences effectively means load external references. Will be removed in v2. External references are never "resolved".
3429
/// </summary>
3530
ResolveAllReferences
3631
}
@@ -43,8 +38,14 @@ public class OpenApiReaderSettings
4338
/// <summary>
4439
/// Indicates how references in the source document should be handled.
4540
/// </summary>
41+
/// <remarks>This setting will be going away in the next major version of this library. Use GetEffective on model objects to get resolved references.</remarks>
4642
public ReferenceResolutionSetting ReferenceResolution { get; set; } = ReferenceResolutionSetting.ResolveLocalReferences;
4743

44+
/// <summary>
45+
/// When external references are found, load them into a shared workspace
46+
/// </summary>
47+
public bool LoadExternalRefs { get; set; } = false;
48+
4849
/// <summary>
4950
/// Dictionary of parsers for converting extensions into strongly typed classes
5051
/// </summary>

src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs

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

4+
using System;
45
using System.IO;
56
using System.Threading.Tasks;
67
using Microsoft.OpenApi.Interfaces;
@@ -23,6 +24,12 @@ public class OpenApiStreamReader : IOpenApiReader<Stream, OpenApiDiagnostic>
2324
public OpenApiStreamReader(OpenApiReaderSettings settings = null)
2425
{
2526
_settings = settings ?? new OpenApiReaderSettings();
27+
28+
if((_settings.ReferenceResolution == ReferenceResolutionSetting.ResolveAllReferences || _settings.LoadExternalRefs)
29+
&& _settings.BaseUrl == null)
30+
{
31+
throw new ArgumentException("BaseUrl must be provided to resolve external references.");
32+
}
2633
}
2734

2835
/// <summary>

src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic
5353
// Parse the OpenAPI Document
5454
document = context.Parse(input);
5555

56+
if (_settings.LoadExternalRefs)
57+
{
58+
throw new InvalidOperationException("Cannot load external refs using the synchronous Read, use ReadAsync instead.");
59+
}
60+
5661
ResolveReferences(diagnostic, document);
5762
}
5863
catch (OpenApiException ex)
@@ -88,7 +93,12 @@ public async Task<ReadResult> ReadAsync(YamlDocument input)
8893
// Parse the OpenAPI Document
8994
document = context.Parse(input);
9095

91-
await ResolveReferencesAsync(diagnostic, document);
96+
if (_settings.LoadExternalRefs)
97+
{
98+
await LoadExternalRefs(document);
99+
}
100+
101+
ResolveReferences(diagnostic, document);
92102
}
93103
catch (OpenApiException ex)
94104
{
@@ -112,52 +122,28 @@ public async Task<ReadResult> ReadAsync(YamlDocument input)
112122
};
113123
}
114124

115-
116-
private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument document)
125+
private async Task LoadExternalRefs(OpenApiDocument document)
117126
{
118-
// Resolve References if requested
119-
switch (_settings.ReferenceResolution)
120-
{
121-
case ReferenceResolutionSetting.ResolveAllReferences:
122-
throw new ArgumentException("Cannot resolve all references via a synchronous call. Use ReadAsync.");
123-
case ReferenceResolutionSetting.ResolveLocalReferences:
124-
var errors = document.ResolveReferences(false);
127+
// Create workspace for all documents to live in.
128+
var openApiWorkSpace = new OpenApiWorkspace();
125129

126-
foreach (var item in errors)
127-
{
128-
diagnostic.Errors.Add(item);
129-
}
130-
break;
131-
case ReferenceResolutionSetting.DoNotResolveReferences:
132-
break;
133-
}
130+
// Load this root document into the workspace
131+
var streamLoader = new DefaultStreamLoader(_settings.BaseUrl);
132+
var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader, _settings);
133+
await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document);
134134
}
135135

136-
private async Task ResolveReferencesAsync(OpenApiDiagnostic diagnostic, OpenApiDocument document)
136+
private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument document)
137137
{
138138
List<OpenApiError> errors = new List<OpenApiError>();
139139

140140
// Resolve References if requested
141141
switch (_settings.ReferenceResolution)
142142
{
143143
case ReferenceResolutionSetting.ResolveAllReferences:
144-
145-
// Create workspace for all documents to live in.
146-
var openApiWorkSpace = new OpenApiWorkspace();
147-
148-
// Load this root document into the workspace
149-
var streamLoader = new DefaultStreamLoader();
150-
var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader, _settings);
151-
await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document);
152-
153-
// Resolve all references in all the documents loaded into the OpenApiWorkspace
154-
foreach (var doc in openApiWorkSpace.Documents)
155-
{
156-
errors.AddRange(doc.ResolveReferences(true));
157-
}
158-
break;
144+
throw new ArgumentException("Resolving external references is not supported");
159145
case ReferenceResolutionSetting.ResolveLocalReferences:
160-
errors.AddRange(document.ResolveReferences(false));
146+
errors.AddRange(document.ResolveReferences());
161147
break;
162148
case ReferenceResolutionSetting.DoNotResolveReferences:
163149
break;

src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,41 @@ namespace Microsoft.OpenApi.Readers.Services
1414
/// </summary>
1515
internal class DefaultStreamLoader : IStreamLoader
1616
{
17+
private readonly Uri baseUrl;
1718
private HttpClient _httpClient = new HttpClient();
1819

20+
21+
public DefaultStreamLoader(Uri baseUrl)
22+
{
23+
this.baseUrl = baseUrl;
24+
}
25+
1926
public Stream Load(Uri uri)
2027
{
28+
var absoluteUri = new Uri(baseUrl, uri);
2129
switch (uri.Scheme)
2230
{
2331
case "file":
24-
return File.OpenRead(uri.AbsolutePath);
32+
return File.OpenRead(absoluteUri.AbsolutePath);
2533
case "http":
2634
case "https":
27-
return _httpClient.GetStreamAsync(uri).GetAwaiter().GetResult();
28-
35+
return _httpClient.GetStreamAsync(absoluteUri).GetAwaiter().GetResult();
2936
default:
3037
throw new ArgumentException("Unsupported scheme");
3138
}
3239
}
3340

3441
public async Task<Stream> LoadAsync(Uri uri)
3542
{
36-
switch (uri.Scheme)
43+
var absoluteUri = new Uri(baseUrl, uri);
44+
45+
switch (absoluteUri.Scheme)
3746
{
3847
case "file":
39-
return File.OpenRead(uri.AbsolutePath);
48+
return File.OpenRead(absoluteUri.AbsolutePath);
4049
case "http":
4150
case "https":
42-
return await _httpClient.GetStreamAsync(uri);
51+
return await _httpClient.GetStreamAsync(absoluteUri);
4352
default:
4453
throw new ArgumentException("Unsupported scheme");
4554
}

0 commit comments

Comments
 (0)