Skip to content

Commit 80931cc

Browse files
dpwdeclego-10-01-06[bot]VisualBeangokerakcByronMayne
authored
feat(references)!: add basic scaffolding for external ref resolution (#168)
Co-authored-by: lego-10-01-06[bot] <119427331+lego-10-01-06[bot]@users.noreply.github.com> Co-authored-by: Alex Wichmann <[email protected]> Co-authored-by: gokerakc <[email protected]> Co-authored-by: VisualBean <[email protected]> Co-authored-by: Byron Mayne <[email protected]> Co-authored-by: James Thompson <[email protected]> Co-authored-by: dec.kolakowski <[email protected]>
1 parent cf6cf6d commit 80931cc

File tree

7 files changed

+461
-14
lines changed

7 files changed

+461
-14
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,48 @@ var stream = await httpClient.GetStreamAsync("master/examples/streetlights-kafka
7878
var asyncApiDocument = new AsyncApiStreamReader().Read(stream, out var diagnostic);
7979
```
8080

81+
#### Reading External $ref
82+
83+
You can read externally referenced AsyncAPI documents by setting the `ReferenceResolution` property of the `AsyncApiReaderSettings` object to `ReferenceResolutionSetting.ResolveAllReferences` and providing an implementation for the `IAsyncApiExternalReferenceReader` interface. This interface contains a single method to which the built AsyncAPI.NET reader library will pass the location content contained in a `$ref` property (usually some form of path) and interface will return the content which is retrieved from wherever the `$ref` points to as a `string`. The AsyncAPI.NET reader will then automatically infer the `T` type of the content and recursively parse the external content into an AsyncAPI document as a child of the original document that contained the `$ref`. This means that you can have externally referenced documents that themselves contain external references.
84+
85+
This interface allows users to load the content of their external reference however and from whereever is required. A new instance of the implementor of `IAsyncApiExternalReferenceReader` should be registered with the `ExternalReferenceReader` property of the `AsyncApiReaderSettings` when creating the reader from which the `GetExternalResource` method will be called when resolving external references.
86+
87+
Below is a very simple example of implementation for `IAsyncApiExternalReferenceReader` that simply loads a file and returns it as a string found at the reference endpoint.
88+
```csharp
89+
using System.IO;
90+
91+
public class AsyncApiExternalFileSystemReader : IAsyncApiExternalReferenceReader
92+
{
93+
public string GetExternalResource(string reference)
94+
{
95+
return File.ReadAllText(reference);
96+
}
97+
}
98+
```
99+
100+
This can then be configured in the reader as follows:
101+
```csharp
102+
var settings = new AsyncApiReaderSettings
103+
{
104+
ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences,
105+
ExternalReferenceReader = new AsyncApiExternalFileSystemReader(),
106+
};
107+
var reader = new AsyncApiStringReader(settings);
108+
```
109+
110+
This would function for a AsyncAPI document with following reference to load the content of `message.yaml` as a `AsyncApiMessage` object inline with the document object.
111+
```yaml
112+
asyncapi: 2.3.0
113+
info:
114+
title: test
115+
version: 1.0.0
116+
channels:
117+
workspace:
118+
publish:
119+
message:
120+
$ref: "../../../message.yaml"
121+
```
122+
81123
### Bindings
82124
To add support for reading bindings, simply add the bindings you wish to support, to the `Bindings` collection of `AsyncApiReaderSettings`.
83125
There is a nifty helper to add different types of bindings, or like in the example `All` of them.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
using LEGO.AsyncAPI.Readers.Exceptions;
2+
using LEGO.AsyncAPI.Services;
3+
4+
namespace LEGO.AsyncAPI.Readers
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using LEGO.AsyncAPI.Exceptions;
10+
using LEGO.AsyncAPI.Models;
11+
using LEGO.AsyncAPI.Models.Interfaces;
12+
13+
/// <summary>
14+
/// This class is used to walk an AsyncApiDocument and convert unresolved references to references to populated objects.
15+
/// </summary>
16+
internal class AsyncApiExternalReferenceResolver : AsyncApiVisitorBase
17+
{
18+
private AsyncApiDocument currentDocument;
19+
private List<AsyncApiError> errors = new List<AsyncApiError>();
20+
private AsyncApiReaderSettings readerSettings;
21+
22+
public AsyncApiExternalReferenceResolver(
23+
AsyncApiDocument currentDocument,
24+
AsyncApiReaderSettings readerSettings)
25+
{
26+
this.currentDocument = currentDocument;
27+
this.readerSettings = readerSettings;
28+
}
29+
30+
public IEnumerable<AsyncApiError> Errors
31+
{
32+
get
33+
{
34+
return this.errors;
35+
}
36+
}
37+
38+
public override void Visit(IAsyncApiReferenceable referenceable)
39+
{
40+
if (referenceable.Reference != null)
41+
{
42+
referenceable.Reference.HostDocument = this.currentDocument;
43+
}
44+
}
45+
46+
public override void Visit(AsyncApiComponents components)
47+
{
48+
this.ResolveMap(components.Parameters);
49+
this.ResolveMap(components.Channels);
50+
this.ResolveMap(components.Schemas);
51+
this.ResolveMap(components.Servers);
52+
this.ResolveMap(components.CorrelationIds);
53+
this.ResolveMap(components.MessageTraits);
54+
this.ResolveMap(components.OperationTraits);
55+
this.ResolveMap(components.SecuritySchemes);
56+
this.ResolveMap(components.ChannelBindings);
57+
this.ResolveMap(components.MessageBindings);
58+
this.ResolveMap(components.OperationBindings);
59+
this.ResolveMap(components.ServerBindings);
60+
this.ResolveMap(components.Messages);
61+
}
62+
63+
public override void Visit(AsyncApiDocument doc)
64+
{
65+
this.ResolveMap(doc.Servers);
66+
this.ResolveMap(doc.Channels);
67+
}
68+
69+
public override void Visit(AsyncApiChannel channel)
70+
{
71+
this.ResolveMap(channel.Parameters);
72+
this.ResolveObject(channel.Bindings, r => channel.Bindings = r);
73+
}
74+
75+
public override void Visit(AsyncApiMessageTrait trait)
76+
{
77+
this.ResolveObject(trait.CorrelationId, r => trait.CorrelationId = r);
78+
this.ResolveObject(trait.Headers, r => trait.Headers = r);
79+
}
80+
81+
/// <summary>
82+
/// Resolve all references used in an operation.
83+
/// </summary>
84+
public override void Visit(AsyncApiOperation operation)
85+
{
86+
this.ResolveList(operation.Message);
87+
this.ResolveList(operation.Traits);
88+
this.ResolveObject(operation.Bindings, r => operation.Bindings = r);
89+
}
90+
91+
public override void Visit(AsyncApiMessage message)
92+
{
93+
this.ResolveObject(message.Headers, r => message.Headers = r);
94+
this.ResolveObject(message.Payload, r => message.Payload = r);
95+
this.ResolveList(message.Traits);
96+
this.ResolveObject(message.CorrelationId, r => message.CorrelationId = r);
97+
this.ResolveObject(message.Bindings, r => message.Bindings = r);
98+
}
99+
100+
public override void Visit(AsyncApiServer server)
101+
{
102+
this.ResolveObject(server.Bindings, r => server.Bindings = r);
103+
}
104+
105+
/// <summary>
106+
/// Resolve all references to SecuritySchemes.
107+
/// </summary>
108+
public override void Visit(AsyncApiSecurityRequirement securityRequirement)
109+
{
110+
foreach (var scheme in securityRequirement.Keys.ToList())
111+
{
112+
this.ResolveObject(scheme, (resolvedScheme) =>
113+
{
114+
if (resolvedScheme != null)
115+
{
116+
// If scheme was unresolved
117+
// copy Scopes and remove old unresolved scheme
118+
var scopes = securityRequirement[scheme];
119+
securityRequirement.Remove(scheme);
120+
securityRequirement.Add(resolvedScheme, scopes);
121+
}
122+
});
123+
}
124+
}
125+
126+
/// <summary>
127+
/// Resolve all references to parameters.
128+
/// </summary>
129+
public override void Visit(IList<AsyncApiParameter> parameters)
130+
{
131+
this.ResolveList(parameters);
132+
}
133+
134+
/// <summary>
135+
/// Resolve all references used in a parameter.
136+
/// </summary>
137+
public override void Visit(AsyncApiParameter parameter)
138+
{
139+
this.ResolveObject(parameter.Schema, r => parameter.Schema = r);
140+
}
141+
142+
/// <summary>
143+
/// Resolve all references used in a schema.
144+
/// </summary>
145+
public override void Visit(AsyncApiSchema schema)
146+
{
147+
this.ResolveObject(schema.Items, r => schema.Items = r);
148+
this.ResolveList(schema.OneOf);
149+
this.ResolveList(schema.AllOf);
150+
this.ResolveList(schema.AnyOf);
151+
this.ResolveObject(schema.Contains, r => schema.Contains = r);
152+
this.ResolveObject(schema.Else, r => schema.Else = r);
153+
this.ResolveObject(schema.If, r => schema.If = r);
154+
this.ResolveObject(schema.Items, r => schema.Items = r);
155+
this.ResolveObject(schema.Not, r => schema.Not = r);
156+
this.ResolveObject(schema.Then, r => schema.Then = r);
157+
this.ResolveObject(schema.PropertyNames, r => schema.PropertyNames = r);
158+
this.ResolveObject(schema.AdditionalProperties, r => schema.AdditionalProperties = r);
159+
this.ResolveMap(schema.Properties);
160+
}
161+
162+
private void ResolveObject<T>(T entity, Action<T> assign)
163+
where T : class, IAsyncApiReferenceable, new()
164+
{
165+
if (entity == null)
166+
{
167+
return;
168+
}
169+
170+
if (this.IsUnresolvedReference(entity))
171+
{
172+
assign(this.ResolveReference<T>(entity.Reference));
173+
}
174+
}
175+
176+
private void ResolveList<T>(IList<T> list)
177+
where T : class, IAsyncApiReferenceable, new()
178+
{
179+
if (list == null)
180+
{
181+
return;
182+
}
183+
184+
for (int i = 0; i < list.Count; i++)
185+
{
186+
var entity = list[i];
187+
if (this.IsUnresolvedReference(entity))
188+
{
189+
list[i] = this.ResolveReference<T>(entity.Reference);
190+
}
191+
}
192+
}
193+
194+
private void ResolveMap<T>(IDictionary<string, T> map)
195+
where T : class, IAsyncApiReferenceable, new()
196+
{
197+
if (map == null)
198+
{
199+
return;
200+
}
201+
202+
foreach (var key in map.Keys.ToList())
203+
{
204+
var entity = map[key];
205+
if (this.IsUnresolvedReference(entity))
206+
{
207+
map[key] = this.ResolveReference<T>(entity.Reference);
208+
}
209+
}
210+
}
211+
212+
private T ResolveReference<T>(AsyncApiReference reference)
213+
where T : class, IAsyncApiReferenceable, new()
214+
{
215+
if (reference.IsExternal)
216+
{
217+
if (this.readerSettings.ExternalReferenceReader is null)
218+
{
219+
throw new AsyncApiReaderException(
220+
"External reference configured in AsyncApi document but no implementation provided for ExternalReferenceReader.");
221+
}
222+
223+
// read external content
224+
var externalContent = this.readerSettings.ExternalReferenceReader.Load(reference.Reference);
225+
226+
// read external object content
227+
var reader = new AsyncApiStringReader(this.readerSettings);
228+
var externalAsyncApiContent = reader.ReadFragment<T>(externalContent, AsyncApiVersion.AsyncApi2_0, out var diagnostic);
229+
foreach (var error in diagnostic.Errors)
230+
{
231+
this.errors.Add(error);
232+
}
233+
234+
return externalAsyncApiContent;
235+
}
236+
237+
return null;
238+
}
239+
240+
private bool IsUnresolvedReference(IAsyncApiReferenceable possibleReference)
241+
{
242+
return (possibleReference != null && possibleReference.UnresolvedReference);
243+
}
244+
}
245+
}

src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) The LEGO Group. All rights reserved.
22

3+
using System;
4+
35
namespace LEGO.AsyncAPI.Readers
46
{
57
using System.Collections.Generic;
@@ -12,6 +14,7 @@ namespace LEGO.AsyncAPI.Readers
1214
using LEGO.AsyncAPI.Models;
1315
using LEGO.AsyncAPI.Models.Interfaces;
1416
using LEGO.AsyncAPI.Readers.Interface;
17+
using LEGO.AsyncAPI.Services;
1518
using LEGO.AsyncAPI.Validations;
1619

1720
/// <summary>
@@ -165,23 +168,48 @@ public T ReadFragment<T>(JsonNode input, AsyncApiVersion version, out AsyncApiDi
165168

166169
private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
167170
{
168-
var errors = new List<AsyncApiError>();
169-
170-
// Resolve References if requested
171171
switch (this.settings.ReferenceResolution)
172172
{
173-
case ReferenceResolutionSetting.ResolveReferences:
174-
errors.AddRange(document.ResolveReferences());
173+
case ReferenceResolutionSetting.ResolveAllReferences:
174+
this.ResolveAllReferences(diagnostic, document);
175+
break;
176+
case ReferenceResolutionSetting.ResolveInternalReferences:
177+
this.ResolveInternalReferences(diagnostic, document);
175178
break;
176-
177179
case ReferenceResolutionSetting.DoNotResolveReferences:
178180
break;
179181
}
182+
}
183+
184+
private void ResolveAllReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
185+
{
186+
this.ResolveInternalReferences(diagnostic, document);
187+
this.ResolveExternalReferences(diagnostic, document);
188+
}
189+
190+
private void ResolveInternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
191+
{
192+
var errors = new List<AsyncApiError>();
193+
194+
var reader = new AsyncApiStringReader(this.settings);
195+
errors.AddRange(document.ResolveReferences());
180196

181197
foreach (var item in errors)
182198
{
183199
diagnostic.Errors.Add(item);
184200
}
185201
}
202+
203+
private void ResolveExternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
204+
{
205+
var resolver = new AsyncApiExternalReferenceResolver(document, this.settings);
206+
var walker = new AsyncApiWalker(resolver);
207+
walker.Walk(document);
208+
209+
foreach (var error in resolver.Errors)
210+
{
211+
diagnostic.Errors.Add(error);
212+
}
213+
}
186214
}
187215
}

0 commit comments

Comments
 (0)