Skip to content

Commit 07aeaaf

Browse files
authored
Merge pull request #123 from BinkyLabs/feat/apply-and-normalize
feat: adds split commands for overlay only and overlay with normalization
2 parents ee3559a + 2a610b0 commit 07aeaaf

File tree

8 files changed

+318
-110
lines changed

8 files changed

+318
-110
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ dotnet add <pathToCsProj> package BinkyLabs.OpenApi.Overlays
2121
The following example illustrates how you can load or parse an Overlay document from JSON or YAML.
2222

2323
```csharp
24-
var overlayDocument = await OverlayDocument.LoadFromUrlAsync("https://source/overlay.json");
24+
var (overlayDocument) = await OverlayDocument.LoadFromUrlAsync("https://source/overlay.json");
2525
```
2626

2727
### Applying an Overlay document to an OpenAPI document
2828

2929
The following example illustrates how you can apply an Overlay document to an OpenAPI document.
3030

3131
```csharp
32-
var resultOpenApiDocument = await overlayDocument.ApplyToDocumentAsync("https://source/openapi.json");
32+
var (resultOpenApiDocument) = await overlayDocument.ApplyToDocumentAndLoadAsync("https://source/openapi.json");
3333
```
3434

3535
### Applying multiple Overlay documents to an OpenAPI document
@@ -39,7 +39,7 @@ The following example illustrates how you can apply multiple Overlay documents t
3939
```csharp
4040
var combinedOverlay = overlayDocument1.CombineWith(overlayDocument2);
4141
// order matters during the combination, the actions will be appended
42-
var resultOpenApiDocument = await combinedOverlay.ApplyToDocumentAsync("https://source/openapi.json");
42+
var (resultOpenApiDocument) = await combinedOverlay.ApplyToDocumentAndLoadAsync("https://source/openapi.json");
4343
```
4444

4545
### Serializing an Overlay document

src/lib/BinkyLabs.OpenApi.Overlays.csproj

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
</PropertyGroup>
3737

3838
<ItemGroup>
39-
<PackageReference Include="JsonPath.Net" Version="2.1.1"/>
40-
<PackageReference Include="JsonPointer.Net" Version="5.3.1"/>
39+
<PackageReference Include="JsonPath.Net" Version="2.1.1" />
40+
<PackageReference Include="JsonPointer.Net" Version="5.3.1" />
4141
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
4242
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4343
<PrivateAssets>all</PrivateAssets>
@@ -46,14 +46,14 @@
4646
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4747
<PrivateAssets>all</PrivateAssets>
4848
</PackageReference>
49-
<PackageReference Include="Microsoft.OpenApi" Version="2.3.7"/>
50-
<None Include="..\..\README.md" Pack="true" PackagePath="README.md"/>
51-
<None Include="..\Assets\logo-binkylabs.png" Pack="true" PackagePath="logo-binkylabs.png"/>
52-
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.3.7"/>
49+
<PackageReference Include="Microsoft.OpenApi" Version="2.3.8" />
50+
<None Include="..\..\README.md" Pack="true" PackagePath="README.md" />
51+
<None Include="..\Assets\logo-binkylabs.png" Pack="true" PackagePath="logo-binkylabs.png" />
52+
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.3.8" />
5353
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
5454
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5555
<PrivateAssets>all</PrivateAssets>
5656
</PackageReference>
57-
<PackageReference Include="SharpYaml" Version="2.1.3"/>
57+
<PackageReference Include="SharpYaml" Version="2.1.4" />
5858
</ItemGroup>
5959
</Project>

src/lib/Models/OverlayDocument.cs

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ internal bool ApplyToDocument(JsonNode jsonNode, OverlayDiagnostic overlayDiagno
126126
/// <param name="readerSettings">Settings to use when reading the document.</param>
127127
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
128128
/// <returns>The OpenAPI document after applying the action.</returns>
129-
public async Task<OverlayApplicationResult> ApplyToExtendedDocumentAsync(string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
129+
public async Task<OverlayApplicationResultOfJsonNode> ApplyToExtendedDocumentAsync(string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
130130
{
131131
if (string.IsNullOrEmpty(Extends))
132132
{
@@ -135,6 +135,24 @@ public async Task<OverlayApplicationResult> ApplyToExtendedDocumentAsync(string?
135135
return await ApplyToDocumentAsync(Extends, format, readerSettings, cancellationToken).ConfigureAwait(false);
136136
}
137137

138+
/// <summary>
139+
/// Applies the action to an OpenAPI document loaded from the extends property.
140+
/// The document is read in the specified format (e.g., JSON or YAML).
141+
/// </summary>
142+
/// <param name="format">The format of the document (e.g., JSON or YAML).</param>
143+
/// <param name="readerSettings">Settings to use when reading the document.</param>
144+
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
145+
/// <returns>The OpenAPI document after applying the action.</returns>
146+
public async Task<OverlayApplicationResultOfOpenApiDocument> ApplyToExtendedDocumentAndLoadAsync(string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
147+
{
148+
if (string.IsNullOrEmpty(Extends))
149+
{
150+
throw new InvalidOperationException("The 'extends' property must be set to apply the overlay to an extended document.");
151+
}
152+
var jsonResult = await ApplyToExtendedDocumentAsync(format, readerSettings, cancellationToken).ConfigureAwait(false);
153+
return LoadDocument(jsonResult, new Uri(Extends), format ?? string.Empty, readerSettings);
154+
}
155+
138156
/// <summary>
139157
/// Applies the action to an OpenAPI document loaded from a specified path or URI.
140158
/// The document is read in the specified format (e.g., JSON or YAML).
@@ -144,7 +162,7 @@ public async Task<OverlayApplicationResult> ApplyToExtendedDocumentAsync(string?
144162
/// <param name="readerSettings">Settings to use when reading the document.</param>
145163
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
146164
/// <returns>The OpenAPI document after applying the action.</returns>
147-
public async Task<OverlayApplicationResult> ApplyToDocumentAsync(string documentPathOrUri, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
165+
public async Task<OverlayApplicationResultOfJsonNode> ApplyToDocumentAsync(string documentPathOrUri, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
148166
{
149167
ArgumentException.ThrowIfNullOrEmpty(documentPathOrUri);
150168
readerSettings ??= new OverlayReaderSettings();
@@ -164,6 +182,23 @@ public async Task<OverlayApplicationResult> ApplyToDocumentAsync(string document
164182
using var fileStream = new FileStream(documentPathOrUri, FileMode.Open, FileAccess.Read);
165183
await fileStream.CopyToAsync(input, cancellationToken).ConfigureAwait(false);
166184
}
185+
var result = await ApplyToDocumentStreamAsync(input, format, readerSettings, cancellationToken).ConfigureAwait(false);
186+
await input.DisposeAsync().ConfigureAwait(false);
187+
return result;
188+
}
189+
190+
/// <summary>
191+
/// Applies the action to an OpenAPI document loaded from a specified path or URI.
192+
/// The document is read in the specified format (e.g., JSON or YAML).
193+
/// </summary>
194+
/// <param name="documentPathOrUri">Path or URI to the OpenAPI document.</param>
195+
/// <param name="format">The format of the document (e.g., JSON or YAML).</param>
196+
/// <param name="readerSettings">Settings to use when reading the document.</param>
197+
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
198+
/// <returns>The OpenAPI document after applying the action.</returns>
199+
public async Task<OverlayApplicationResultOfOpenApiDocument> ApplyToDocumentAndLoadAsync(string documentPathOrUri, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
200+
{
201+
var jsonResult = await ApplyToDocumentAsync(documentPathOrUri, format, readerSettings, cancellationToken).ConfigureAwait(false);
167202

168203
// Convert file paths to absolute paths before creating URI to handle relative paths correctly
169204
Uri uri;
@@ -178,22 +213,20 @@ public async Task<OverlayApplicationResult> ApplyToDocumentAsync(string document
178213
var absolutePath = Path.GetFullPath(documentPathOrUri);
179214
uri = new Uri(absolutePath, UriKind.Absolute);
180215
}
181-
var result = await ApplyToDocumentStreamAsync(input, uri, format, readerSettings, cancellationToken).ConfigureAwait(false);
182-
await input.DisposeAsync().ConfigureAwait(false);
183-
return result;
216+
217+
return LoadDocument(jsonResult, uri, format ?? string.Empty, readerSettings);
184218
}
185219

186220
/// <summary>
187221
/// Applies the action to an OpenAPI document loaded from a specified path or URI.
188222
/// The document is read in the specified format (e.g., JSON or YAML).
189223
/// </summary>
190224
/// <param name="input">A stream containing the OpenAPI document.</param>
191-
/// <param name="location">The URI location of the document, used for to load external references.</param>
192225
/// <param name="format">The format of the document (e.g., JSON or YAML).</param>
193226
/// <param name="readerSettings">Settings to use when reading the document.</param>
194227
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
195228
/// <returns>The OpenAPI document after applying the action.</returns>
196-
public async Task<OverlayApplicationResult> ApplyToDocumentStreamAsync(Stream input, Uri location, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
229+
public async Task<OverlayApplicationResultOfJsonNode> ApplyToDocumentStreamAsync(Stream input, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
197230
{
198231
ArgumentNullException.ThrowIfNull(input);
199232
readerSettings ??= new OverlayReaderSettings();
@@ -213,21 +246,48 @@ public async Task<OverlayApplicationResult> ApplyToDocumentStreamAsync(Stream in
213246
throw new InvalidOperationException("Failed to parse the OpenAPI document.");
214247
var overlayDiagnostic = new OverlayDiagnostic();
215248
var result = ApplyToDocument(jsonNode, overlayDiagnostic);
216-
var openAPIJsonReader = new OpenApiJsonReader();
217-
var (openAPIDocument, openApiDiagnostic) = openAPIJsonReader.Read(jsonNode, location, readerSettings.OpenApiSettings);
218-
if (openApiDiagnostic is not null)
249+
return new OverlayApplicationResultOfJsonNode
219250
{
220-
openApiDiagnostic.Format = format;
221-
}
222-
return new OverlayApplicationResult
223-
{
224-
Document = openAPIDocument,
251+
Document = jsonNode,
225252
Diagnostic = overlayDiagnostic,
226-
OpenApiDiagnostic = openApiDiagnostic,
227253
IsSuccessful = result,
254+
OpenApiDiagnostic = new OpenApiDiagnostic()
255+
{
256+
Format = format
257+
}
228258
};
229259
}
230260
/// <summary>
261+
/// Applies the action to an OpenAPI document loaded from a specified path or URI.
262+
/// The document is read in the specified format (e.g., JSON or YAML).
263+
/// </summary>
264+
/// <param name="input">A stream containing the OpenAPI document.</param>
265+
/// <param name="location">The URI location of the document, used for to load external references.</param>
266+
/// <param name="format">The format of the document (e.g., JSON or YAML).</param>
267+
/// <param name="readerSettings">Settings to use when reading the document.</param>
268+
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
269+
/// <returns>The OpenAPI document after applying the action.</returns>
270+
public async Task<OverlayApplicationResultOfOpenApiDocument> ApplyToDocumentStreamAndLoadAsync(Stream input, Uri location, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default)
271+
{
272+
var jsonResult = await ApplyToDocumentStreamAsync(input, format, readerSettings, cancellationToken).ConfigureAwait(false);
273+
return LoadDocument(jsonResult, location, format ?? string.Empty, readerSettings);
274+
}
275+
internal static OverlayApplicationResultOfOpenApiDocument LoadDocument(OverlayApplicationResultOfJsonNode jsonResult, Uri location, string format, OverlayReaderSettings? readerSettings)
276+
{
277+
readerSettings ??= new OverlayReaderSettings();
278+
var openAPIJsonReader = new OpenApiJsonReader();
279+
if (jsonResult.Document is null)
280+
{
281+
return OverlayApplicationResultOfOpenApiDocument.FromJsonResultWithFailedLoad(jsonResult);
282+
}
283+
var (openAPIDocument, openApiDiagnostic) = openAPIJsonReader.Read(jsonResult.Document, location, readerSettings.OpenApiSettings);
284+
if (openApiDiagnostic is not null && !string.IsNullOrEmpty(format))
285+
{
286+
openApiDiagnostic.Format = format;
287+
}
288+
return OverlayApplicationResultOfOpenApiDocument.FromJsonResult(jsonResult, openAPIDocument, openApiDiagnostic);
289+
}
290+
/// <summary>
231291
/// Combines this overlay document with another overlay document.
232292
/// The returned document will be a new document, and its metadata (info, etc.) will be the one from the other document.
233293
/// The actions from both documents will be merged. The current document actions will be first, and the ones from the other document will be next.

src/lib/OverlayApplicationResult.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text.Json.Nodes;
2+
13
using BinkyLabs.OpenApi.Overlays.Reader;
24

35
using Microsoft.OpenApi;
@@ -8,12 +10,50 @@ namespace BinkyLabs.OpenApi.Overlays;
810
/// <summary>
911
/// Result of applying overlays to an OpenAPI document
1012
/// </summary>
11-
public class OverlayApplicationResult
13+
public class OverlayApplicationResultOfOpenApiDocument : OverlayApplicationResult<OpenApiDocument>
14+
{
15+
internal static OverlayApplicationResultOfOpenApiDocument FromJsonResultWithFailedLoad(OverlayApplicationResultOfJsonNode jsonResult)
16+
{
17+
ArgumentNullException.ThrowIfNull(jsonResult);
18+
return new OverlayApplicationResultOfOpenApiDocument
19+
{
20+
Document = null,
21+
Diagnostic = jsonResult.Diagnostic,
22+
// maintains source format information
23+
OpenApiDiagnostic = jsonResult.OpenApiDiagnostic,
24+
IsSuccessful = false,
25+
};
26+
}
27+
internal static OverlayApplicationResultOfOpenApiDocument FromJsonResult(OverlayApplicationResultOfJsonNode jsonResult, OpenApiDocument? document, OpenApiDiagnostic? openApiDiagnostic)
28+
{
29+
ArgumentNullException.ThrowIfNull(jsonResult);
30+
return new OverlayApplicationResultOfOpenApiDocument
31+
{
32+
Document = document,
33+
Diagnostic = jsonResult.Diagnostic,
34+
// maintains source format information
35+
OpenApiDiagnostic = openApiDiagnostic ?? jsonResult.OpenApiDiagnostic,
36+
IsSuccessful = jsonResult.IsSuccessful,
37+
};
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Result of applying overlays to an OpenAPI document
43+
/// </summary>
44+
public class OverlayApplicationResultOfJsonNode : OverlayApplicationResult<JsonNode>
45+
{
46+
}
47+
48+
/// <summary>
49+
/// Result of applying overlays to an OpenAPI document
50+
/// </summary>
51+
public class OverlayApplicationResult<T>
1252
{
1353
/// <summary>
1454
/// The resulting OpenAPI document after applying overlays, or null if application failed
1555
/// </summary>
16-
public OpenApiDocument? Document { get; init; }
56+
public T? Document { get; init; }
1757
/// <summary>
1858
/// Diagnostics from applying the overlays
1959
/// </summary>
@@ -35,7 +75,7 @@ public class OverlayApplicationResult
3575
/// <param name="diagnostic">Diagnostics from applying the overlays</param>
3676
/// <param name="openApiDiagnostic">Diagnostics from reading the updated OpenAPI document</param>
3777
/// <param name="isSuccessful">Indicates whether the overlay application was successful</param>
38-
public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic, out bool isSuccessful)
78+
public void Deconstruct(out T? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic, out bool isSuccessful)
3979
{
4080
document = Document;
4181
diagnostic = Diagnostic;
@@ -48,7 +88,7 @@ public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic dia
4888
/// <param name="document">The resulting OpenAPI document after applying overlays, or null if application failed</param>
4989
/// <param name="diagnostic">Diagnostics from applying the overlays</param>
5090
/// <param name="openApiDiagnostic">Diagnostics from reading the updated OpenAPI document</param>
51-
public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic)
91+
public void Deconstruct(out T? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic)
5292
{
5393
Deconstruct(out document, out diagnostic, out openApiDiagnostic, out _);
5494
}
@@ -57,15 +97,15 @@ public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic dia
5797
/// </summary>
5898
/// <param name="document">The resulting OpenAPI document after applying overlays, or null if application failed</param>
5999
/// <param name="diagnostic">Diagnostics from applying the overlays</param>
60-
public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic diagnostic)
100+
public void Deconstruct(out T? document, out OverlayDiagnostic diagnostic)
61101
{
62102
Deconstruct(out document, out diagnostic, out _);
63103
}
64104
/// <summary>
65105
/// Deconstructs the OverlayApplicationResult into its components
66106
/// </summary>
67107
/// <param name="document">The resulting OpenAPI document after applying overlays, or null if application failed</param>
68-
public void Deconstruct(out OpenApiDocument? document)
108+
public void Deconstruct(out T? document)
69109
{
70110
Deconstruct(out document, out _);
71111
}

0 commit comments

Comments
 (0)