Skip to content

Commit

Permalink
feat(references)!: add basic scaffolding for external ref resolution (#…
Browse files Browse the repository at this point in the history
…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]>

BREAKING CHANGE: Change the `ReferenceResolution` enum.
  • Loading branch information
dpwdec authored and VisualBean committed Jun 14, 2024
1 parent cf6cf6d commit 559d002
Show file tree
Hide file tree
Showing 7 changed files with 461 additions and 14 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,48 @@ var stream = await httpClient.GetStreamAsync("master/examples/streetlights-kafka
var asyncApiDocument = new AsyncApiStreamReader().Read(stream, out var diagnostic);
```

#### Reading External $ref

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.

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.

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.
```csharp
using System.IO;

public class AsyncApiExternalFileSystemReader : IAsyncApiExternalReferenceReader
{
public string GetExternalResource(string reference)
{
return File.ReadAllText(reference);
}
}
```

This can then be configured in the reader as follows:
```csharp
var settings = new AsyncApiReaderSettings
{
ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences,
ExternalReferenceReader = new AsyncApiExternalFileSystemReader(),
};
var reader = new AsyncApiStringReader(settings);
```

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.
```yaml
asyncapi: 2.3.0
info:
title: test
version: 1.0.0
channels:
workspace:
publish:
message:
$ref: "../../../message.yaml"
```
### Bindings
To add support for reading bindings, simply add the bindings you wish to support, to the `Bindings` collection of `AsyncApiReaderSettings`.
There is a nifty helper to add different types of bindings, or like in the example `All` of them.
Expand Down
245 changes: 245 additions & 0 deletions src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
using LEGO.AsyncAPI.Readers.Exceptions;
using LEGO.AsyncAPI.Services;

namespace LEGO.AsyncAPI.Readers
{
using System;
using System.Collections.Generic;
using System.Linq;
using LEGO.AsyncAPI.Exceptions;
using LEGO.AsyncAPI.Models;
using LEGO.AsyncAPI.Models.Interfaces;

/// <summary>
/// This class is used to walk an AsyncApiDocument and convert unresolved references to references to populated objects.
/// </summary>
internal class AsyncApiExternalReferenceResolver : AsyncApiVisitorBase
{
private AsyncApiDocument currentDocument;
private List<AsyncApiError> errors = new List<AsyncApiError>();
private AsyncApiReaderSettings readerSettings;

public AsyncApiExternalReferenceResolver(
AsyncApiDocument currentDocument,
AsyncApiReaderSettings readerSettings)
{
this.currentDocument = currentDocument;
this.readerSettings = readerSettings;
}

public IEnumerable<AsyncApiError> Errors
{
get
{
return this.errors;
}
}

public override void Visit(IAsyncApiReferenceable referenceable)
{
if (referenceable.Reference != null)
{
referenceable.Reference.HostDocument = this.currentDocument;
}
}

public override void Visit(AsyncApiComponents components)
{
this.ResolveMap(components.Parameters);
this.ResolveMap(components.Channels);
this.ResolveMap(components.Schemas);
this.ResolveMap(components.Servers);
this.ResolveMap(components.CorrelationIds);
this.ResolveMap(components.MessageTraits);
this.ResolveMap(components.OperationTraits);
this.ResolveMap(components.SecuritySchemes);
this.ResolveMap(components.ChannelBindings);
this.ResolveMap(components.MessageBindings);
this.ResolveMap(components.OperationBindings);
this.ResolveMap(components.ServerBindings);
this.ResolveMap(components.Messages);
}

public override void Visit(AsyncApiDocument doc)
{
this.ResolveMap(doc.Servers);
this.ResolveMap(doc.Channels);
}

public override void Visit(AsyncApiChannel channel)
{
this.ResolveMap(channel.Parameters);
this.ResolveObject(channel.Bindings, r => channel.Bindings = r);
}

public override void Visit(AsyncApiMessageTrait trait)
{
this.ResolveObject(trait.CorrelationId, r => trait.CorrelationId = r);
this.ResolveObject(trait.Headers, r => trait.Headers = r);
}

/// <summary>
/// Resolve all references used in an operation.
/// </summary>
public override void Visit(AsyncApiOperation operation)
{
this.ResolveList(operation.Message);
this.ResolveList(operation.Traits);
this.ResolveObject(operation.Bindings, r => operation.Bindings = r);
}

public override void Visit(AsyncApiMessage message)
{
this.ResolveObject(message.Headers, r => message.Headers = r);
this.ResolveObject(message.Payload, r => message.Payload = r);
this.ResolveList(message.Traits);
this.ResolveObject(message.CorrelationId, r => message.CorrelationId = r);
this.ResolveObject(message.Bindings, r => message.Bindings = r);
}

public override void Visit(AsyncApiServer server)
{
this.ResolveObject(server.Bindings, r => server.Bindings = r);
}

/// <summary>
/// Resolve all references to SecuritySchemes.
/// </summary>
public override void Visit(AsyncApiSecurityRequirement securityRequirement)
{
foreach (var scheme in securityRequirement.Keys.ToList())
{
this.ResolveObject(scheme, (resolvedScheme) =>
{
if (resolvedScheme != null)
{
// If scheme was unresolved
// copy Scopes and remove old unresolved scheme
var scopes = securityRequirement[scheme];
securityRequirement.Remove(scheme);
securityRequirement.Add(resolvedScheme, scopes);
}
});
}
}

/// <summary>
/// Resolve all references to parameters.
/// </summary>
public override void Visit(IList<AsyncApiParameter> parameters)
{
this.ResolveList(parameters);
}

/// <summary>
/// Resolve all references used in a parameter.
/// </summary>
public override void Visit(AsyncApiParameter parameter)
{
this.ResolveObject(parameter.Schema, r => parameter.Schema = r);
}

/// <summary>
/// Resolve all references used in a schema.
/// </summary>
public override void Visit(AsyncApiSchema schema)
{
this.ResolveObject(schema.Items, r => schema.Items = r);
this.ResolveList(schema.OneOf);
this.ResolveList(schema.AllOf);
this.ResolveList(schema.AnyOf);
this.ResolveObject(schema.Contains, r => schema.Contains = r);
this.ResolveObject(schema.Else, r => schema.Else = r);
this.ResolveObject(schema.If, r => schema.If = r);
this.ResolveObject(schema.Items, r => schema.Items = r);
this.ResolveObject(schema.Not, r => schema.Not = r);
this.ResolveObject(schema.Then, r => schema.Then = r);
this.ResolveObject(schema.PropertyNames, r => schema.PropertyNames = r);
this.ResolveObject(schema.AdditionalProperties, r => schema.AdditionalProperties = r);
this.ResolveMap(schema.Properties);
}

private void ResolveObject<T>(T entity, Action<T> assign)
where T : class, IAsyncApiReferenceable, new()
{
if (entity == null)
{
return;
}

if (this.IsUnresolvedReference(entity))
{
assign(this.ResolveReference<T>(entity.Reference));
}
}

private void ResolveList<T>(IList<T> list)
where T : class, IAsyncApiReferenceable, new()
{
if (list == null)
{
return;
}

for (int i = 0; i < list.Count; i++)
{
var entity = list[i];
if (this.IsUnresolvedReference(entity))
{
list[i] = this.ResolveReference<T>(entity.Reference);
}
}
}

private void ResolveMap<T>(IDictionary<string, T> map)
where T : class, IAsyncApiReferenceable, new()
{
if (map == null)
{
return;
}

foreach (var key in map.Keys.ToList())
{
var entity = map[key];
if (this.IsUnresolvedReference(entity))
{
map[key] = this.ResolveReference<T>(entity.Reference);
}
}
}

private T ResolveReference<T>(AsyncApiReference reference)
where T : class, IAsyncApiReferenceable, new()
{
if (reference.IsExternal)
{
if (this.readerSettings.ExternalReferenceReader is null)
{
throw new AsyncApiReaderException(
"External reference configured in AsyncApi document but no implementation provided for ExternalReferenceReader.");
}

// read external content
var externalContent = this.readerSettings.ExternalReferenceReader.Load(reference.Reference);

// read external object content
var reader = new AsyncApiStringReader(this.readerSettings);
var externalAsyncApiContent = reader.ReadFragment<T>(externalContent, AsyncApiVersion.AsyncApi2_0, out var diagnostic);
foreach (var error in diagnostic.Errors)
{
this.errors.Add(error);
}

return externalAsyncApiContent;
}

return null;
}

private bool IsUnresolvedReference(IAsyncApiReferenceable possibleReference)
{
return (possibleReference != null && possibleReference.UnresolvedReference);
}
}
}
40 changes: 34 additions & 6 deletions src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) The LEGO Group. All rights reserved.

using System;

namespace LEGO.AsyncAPI.Readers
{
using System.Collections.Generic;
Expand All @@ -12,6 +14,7 @@ namespace LEGO.AsyncAPI.Readers
using LEGO.AsyncAPI.Models;
using LEGO.AsyncAPI.Models.Interfaces;
using LEGO.AsyncAPI.Readers.Interface;
using LEGO.AsyncAPI.Services;
using LEGO.AsyncAPI.Validations;

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

private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
{
var errors = new List<AsyncApiError>();

// Resolve References if requested
switch (this.settings.ReferenceResolution)
{
case ReferenceResolutionSetting.ResolveReferences:
errors.AddRange(document.ResolveReferences());
case ReferenceResolutionSetting.ResolveAllReferences:
this.ResolveAllReferences(diagnostic, document);
break;
case ReferenceResolutionSetting.ResolveInternalReferences:
this.ResolveInternalReferences(diagnostic, document);
break;

case ReferenceResolutionSetting.DoNotResolveReferences:
break;
}
}

private void ResolveAllReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
{
this.ResolveInternalReferences(diagnostic, document);
this.ResolveExternalReferences(diagnostic, document);
}

private void ResolveInternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
{
var errors = new List<AsyncApiError>();

var reader = new AsyncApiStringReader(this.settings);
errors.AddRange(document.ResolveReferences());

foreach (var item in errors)
{
diagnostic.Errors.Add(item);
}
}

private void ResolveExternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document)
{
var resolver = new AsyncApiExternalReferenceResolver(document, this.settings);
var walker = new AsyncApiWalker(resolver);
walker.Walk(document);

foreach (var error in resolver.Errors)
{
diagnostic.Errors.Add(error);
}
}
}
}
Loading

0 comments on commit 559d002

Please sign in to comment.