Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ namespace ByteBard.AsyncAPI.Readers
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Models.Interfaces;
using ByteBard.AsyncAPI.Readers.Interface;
using ByteBard.AsyncAPI.Readers.ParseNodes;
using ByteBard.AsyncAPI.Readers.Services;
using ByteBard.AsyncAPI.Services;
using ByteBard.AsyncAPI.Validations;
Expand Down Expand Up @@ -381,17 +380,17 @@ private IAsyncApiSerializable ResolveStreamReference(Stream stream, IAsyncApiRef
}

var pointerResult = pointer.Find(json);
if (pointerResult != null)
{
json = pointerResult;
}
else
if (pointerResult == null)
{
diagnostic.Errors.Add(new AsyncApiError(reference.Reference.Reference, "Could not resolve reference fragment."));
return null;
}

json = pointerResult;
}

json = this.context.VersionService.PrepareReferenceFragment(json, reference, this.context);

AsyncApiDiagnostic fragmentDiagnostic = new AsyncApiDiagnostic
{
SpecificationVersion = diagnostic.SpecificationVersion,
Expand Down Expand Up @@ -453,6 +452,21 @@ private IAsyncApiSerializable ResolveStreamReference(Stream stream, IAsyncApiRef
case ReferenceType.MessageBindings:
result = this.ReadFragment<AsyncApiBindings<IMessageBinding>>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic);
break;
case ReferenceType.ServerVariable:
result = this.ReadFragment<AsyncApiServerVariable>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic);
break;
case ReferenceType.Tag:
result = this.ReadFragment<AsyncApiTag>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic);
break;
case ReferenceType.ExternalDocs:
result = this.ReadFragment<AsyncApiExternalDocumentation>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic);
break;
case ReferenceType.OperationReply when diagnostic.SpecificationVersion == AsyncApiVersion.AsyncApi3_0:
result = this.ReadFragment<AsyncApiOperationReply>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic);
break;
case ReferenceType.OperationReplyAddress when diagnostic.SpecificationVersion == AsyncApiVersion.AsyncApi3_0:
result = this.ReadFragment<AsyncApiOperationReplyAddress>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic);
break;
default:
diagnostic.Errors.Add(new AsyncApiError(reference.Reference.Reference, "Could not resolve reference."));
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace ByteBard.AsyncAPI.Readers.Interface
{
using System.Text.Json.Nodes;
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Models.Interfaces;
using ByteBard.AsyncAPI.Readers.ParseNodes;
Expand All @@ -12,5 +13,10 @@ T LoadElement<T>(ParseNode node)
where T : IAsyncApiElement;

AsyncApiDocument LoadDocument(RootNode rootNode);

/// <summary>
/// Adjusts the fragment a reference points at before it is loaded, for what the version keeps elsewhere.
/// </summary>
JsonNode PrepareReferenceFragment(JsonNode fragment, IAsyncApiReferenceable reference, ParsingContext context);
}
}
}

This file was deleted.

2 changes: 1 addition & 1 deletion src/ByteBard.AsyncAPI.Readers/ParsingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ internal AsyncApiDocument Parse(JsonNode jsonNode)

this.Workspace.SetRootDocument(doc);
this.Workspace.RegisterComponents(doc);
this.Workspace.RegisterComponent(string.Empty, this.ParseToStream(doc));
this.Workspace.RegisterComponent(string.Empty, this.ParseToStream(jsonNode));

this.Diagnostic.SpecificationVersion = AsyncApiVersion.AsyncApi2_0;
break;
Expand Down
1 change: 1 addition & 0 deletions src/ByteBard.AsyncAPI.Readers/TempStorageKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public static class TempStorageKeys
public const string ComponentMessages = "ComponentMessages";
public const string ChannelAddresses = "ChannelAddresses";
public const string ParameterSchemaReferences = "ParameterSchemaReferences";
public const string PayloadSchemaFormat = "PayloadSchemaFormat";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static AsyncApiChannel LoadChannel(ParseNode node, string channelAddress
var pointer = mapNode.GetReferencePointer();
if (pointer != null)
{
return new AsyncApiChannelReference(pointer);
return new AsyncApiChannelReference(NormalizeChannelPointer(pointer));
}

var channel = new AsyncApiChannel();
Expand All @@ -53,6 +53,23 @@ public static AsyncApiChannel LoadChannel(ParseNode node, string channelAddress
return channel;
}

/// <summary>
/// 2.x keys channels by address, which is normalized into the channel's key, so a reference to a root channel
/// is pointed at that key.
/// </summary>
private static string NormalizeChannelPointer(string pointer)
{
const string channelsPrefix = "#/channels/";
if (!pointer.StartsWith(channelsPrefix)
|| !JsonPointer.TryFromUriFragment(pointer.Substring(1), out var parsed)
|| parsed.Tokens.Length != 2)
{
return pointer;
}

return channelsPrefix + JsonPointer.EscapeToken(NormalizeChannelKey(parsed.Tokens[1]));
}

public static string NormalizeChannelKey(string channelKey, ParseNode node = null)
{
string newKey = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,14 @@ public static AsyncApiMessage LoadMessage(ParseNode node)
var payload = LoadPayload(mapNode["payload"].Value, schema);
if (payload is IAsyncApiReferenceable reference && !reference.Reference.IsExternal)
{
message.Payload = new AsyncApiMultiFormatSchemaReference(reference.Reference.Reference);
var payloadReference = new AsyncApiMultiFormatSchemaReference(reference.Reference.Reference);
message.Payload = payloadReference;

// 2.x keeps the schema format on the message, so keep it for when the reference is resolved.
if (schema != null)
{
node.Context.SetTempStorage(TempStorageKeys.PayloadSchemaFormat, schema, payloadReference);
}
}
else
{
Expand Down
27 changes: 23 additions & 4 deletions src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace ByteBard.AsyncAPI.Readers.V2
namespace ByteBard.AsyncAPI.Readers.V2
{
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using ByteBard.AsyncAPI.Exceptions;
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Models.Interfaces;
Expand Down Expand Up @@ -32,12 +33,11 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic)
[typeof(AsyncApiOAuthFlows)] = AsyncApiV2Deserializer.LoadOAuthFlows,
[typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation,
[typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter,
[typeof(AsyncApiJsonSchema)] = AsyncApiJsonSchemaDeserializer.LoadSchema,
[typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema,
[typeof(AsyncApiJsonSchema)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload,
[typeof(AsyncApiAvroSchema)] = AsyncApiV2Deserializer.LoadAvroPayload,
// 2.x has no Multi Format Schema Object. Only the local reference fallback asks for one, and it reads the document after the upgrade to 3.0.
[typeof(AsyncApiMultiFormatSchema)] = AsyncApiV3Deserializer.LoadMultiFormatSchema,
[typeof(AsyncApiCorrelationId)] = AsyncApiV2Deserializer.LoadCorrelationId,
[typeof(AsyncApiOperationTrait)] = AsyncApiV2Deserializer.LoadOperationTrait,
[typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme,
[typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer,
[typeof(AsyncApiServerVariable)] = AsyncApiV2Deserializer.LoadServerVariable,
Expand Down Expand Up @@ -81,6 +81,25 @@ public AsyncApiDocument LoadDocument(RootNode rootNode)
return AsyncApiV2Deserializer.LoadAsyncApi(rootNode);
}

/// <summary>
/// 2.x keeps a payload's schema format on its message, which the reader stored with the payload reference.
/// Wrap the payload with it as a Multi Format Schema Object, so it is read in that format.
/// </summary>
public JsonNode PrepareReferenceFragment(JsonNode fragment, IAsyncApiReferenceable reference, ParsingContext context)
{
if (reference is not AsyncApiMultiFormatSchemaReference
|| context.GetFromTempStorage<string>(TempStorageKeys.PayloadSchemaFormat, reference) is not string schemaFormat)
{
return fragment;
}

return new JsonObject
{
["schemaFormat"] = schemaFormat,
["schema"] = fragment.DeepClone(),
};
}

public T LoadElement<T>(ParseNode node)
where T : IAsyncApiElement
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using ByteBard.AsyncAPI.Exceptions;
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Models.Interfaces;
Expand Down Expand Up @@ -38,6 +39,8 @@ public AsyncApiV3VersionService(AsyncApiDiagnostic diagnostic)
[typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema,
[typeof(AsyncApiSecurityScheme)] = AsyncApiV3Deserializer.LoadSecurityScheme,
[typeof(AsyncApiMultiFormatSchema)] = AsyncApiV3Deserializer.LoadMultiFormatSchema,
[typeof(AsyncApiCorrelationId)] = AsyncApiV3Deserializer.LoadCorrelationId,
[typeof(AsyncApiOperationTrait)] = AsyncApiV3Deserializer.LoadOperationTrait,
[typeof(AsyncApiServer)] = AsyncApiV3Deserializer.LoadServer,
[typeof(AsyncApiServerVariable)] = AsyncApiV3Deserializer.LoadServerVariable,
[typeof(AsyncApiTag)] = AsyncApiV3Deserializer.LoadTag,
Expand Down Expand Up @@ -80,6 +83,11 @@ public AsyncApiDocument LoadDocument(RootNode rootNode)
return AsyncApiV3Deserializer.LoadAsyncApi(rootNode);
}

public JsonNode PrepareReferenceFragment(JsonNode fragment, IAsyncApiReferenceable reference, ParsingContext context)
{
return fragment;
}

public T LoadElement<T>(ParseNode node)
where T : IAsyncApiElement
{
Expand Down
35 changes: 33 additions & 2 deletions src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ public void RegisterComponents(AsyncApiDocument document)
this.RegisterComponent(location, item.Value);
}

// Register references last, so one pointing at another root channel or server finds it registered.
string channelBaseUri = "#/channels/";
foreach (var channel in document.Channels)
foreach (var channel in document.Channels.OrderBy(channel => channel.Value is IAsyncApiReferenceable))
{
var registerableChannelValue = channel.Value;
if (channel.Value is IAsyncApiReferenceable reference)
Expand All @@ -196,6 +197,12 @@ public void RegisterComponents(AsyncApiDocument document)
registerableChannelValue = this.ResolveReference<AsyncApiChannel>(reference.Reference);
}

// A reference to a channel that is not registered (yet) is resolved by the reader later.
if (registerableChannelValue == null)
{
continue;
}

location = channelBaseUri + JsonPointer.EscapeToken(channel.Key);
this.RegisterComponent(location, registerableChannelValue);

Expand All @@ -206,7 +213,7 @@ public void RegisterComponents(AsyncApiDocument document)
}

string serverBaseUri = "#/servers/";
foreach (var server in document.Servers)
foreach (var server in document.Servers.OrderBy(server => server.Value is IAsyncApiReferenceable))
{
var registerableServerValue = server.Value;
if (server.Value is IAsyncApiReferenceable serverReference)
Expand Down Expand Up @@ -287,6 +294,30 @@ private Uri ToLocationUrl(string location)
public void SetRootDocument(AsyncApiDocument doc)
{
this.RootDocument = doc;
doc.SourceWorkspace = this;
}

/// <summary>
/// Prepares this workspace for writing the document: registers its components, then adds what the workspace
/// the document was read into resolved outside them, so references to those can still be written inline.
/// </summary>
internal void RegisterForWriting(AsyncApiDocument doc)
{
this.RootDocument = doc;
this.RegisterComponents(doc);

if (doc.SourceWorkspace == null || doc.SourceWorkspace == this)
{
return;
}

foreach (var resolved in doc.SourceWorkspace.resolvedReferenceRegistry)
{
if (!this.resolvedReferenceRegistry.ContainsKey(resolved.Key))
{
this.resolvedReferenceRegistry[resolved.Key] = resolved.Value;
}
}
}
}
}
34 changes: 34 additions & 0 deletions src/ByteBard.AsyncAPI/JsonPointer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
namespace ByteBard.AsyncAPI
{
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json.Nodes;
using ByteBard.AsyncAPI.Exceptions;

/// <summary>
Expand Down Expand Up @@ -117,6 +119,38 @@ public static string EscapeToken(string token)
return token.Replace("~", "~0").Replace("/", "~1");
}

/// <summary>
/// Finds the node this pointer references.
/// </summary>
/// <param name="document">The document to evaluate the pointer against.</param>
/// <returns>The referenced node, or <c>null</c> when it does not exist.</returns>
public JsonNode Find(JsonNode document)
{
var node = document;
foreach (var token in this.Tokens)
{
if (node is JsonArray sequence)
{
// RFC 6901 array indices are "0" or digits without a leading zero.
if ((token.Length > 1 && token[0] == '0')
|| !int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out var index)
|| index >= sequence.Count)
{
return null;
}

node = sequence[index];
}
else if (node is not JsonObject map || !map.TryGetPropertyValue(token, out node))
{
// The token walks into a scalar or a missing property.
return null;
}
}

return node;
}

/// <summary>
/// Gets the JSON string representation of this JSON pointer.
/// </summary>
Expand Down
Loading
Loading