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
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal static partial class AsyncApiV2Deserializer
private static string GetServerReferenceKey(ValueNode valueNode)
{
var stringValue = valueNode.GetScalarValue();
return stringValue.StartsWith("#/servers/") ? stringValue : "#/servers/" + stringValue;
return stringValue.StartsWith("#/servers/") ? stringValue : "#/servers/" + JsonPointer.EscapeToken(stringValue);
}

private static readonly PatternFieldMap<AsyncApiChannel> ChannelPatternFields =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private static void SetOperations(ParsingContext context, AsyncApiDocument docum
var correctChannelReference = document.Channels.FirstOrDefault(channel => channel.Value is AsyncApiChannelReference reference && reference.Reference.Reference.EndsWith(operationChannelFragmentKey));
if (correctChannelReference.Key != null)
{
operation.Value.Channel = new AsyncApiChannelReference("#/channels/" + correctChannelReference.Key);
operation.Value.Channel = new AsyncApiChannelReference("#/channels/" + JsonPointer.EscapeToken(correctChannelReference.Key));
channel = correctChannelReference;
}
else
Expand All @@ -161,7 +161,7 @@ private static void SetOperations(ParsingContext context, AsyncApiDocument docum
foreach (var message in messages)
{
channel.Value.Messages.TryAdd(message.Key, message.Value);
operation.Value.Messages.Add(new AsyncApiMessageReference($"#/channels/{channel.Key}/messages/{message.Key}"));
operation.Value.Messages.Add(new AsyncApiMessageReference($"#/channels/{JsonPointer.EscapeToken(channel.Key)}/messages/{JsonPointer.EscapeToken(message.Key)}"));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ private static KeyValuePair<string, AsyncApiMessage> GetMessage(ParseNode node)
var message = LoadMessage(node);
if (message is AsyncApiMessageReference reference)
{
key = reference.Reference.Reference.Split('/').Last();
// Use the unescaped last pointer token, so the key matches the referenced component's key.
key = reference.Reference.IsFragment && JsonPointer.TryFromUriFragment(reference.Reference.FragmentId, out var pointer) && pointer.Tokens.Length > 0
? pointer.Tokens.Last()
: reference.Reference.Reference.Split('/').Last();
}
else if (messageNode["messageId"] != null)
{
Expand Down Expand Up @@ -87,7 +90,7 @@ private static void LoadMessages(ParseNode n, AsyncApiOperation instance)
{
if (message.Value is not AsyncApiMessageReference messageReference)
{
var componentReference = "#/components/messages/" + message.Key;
var componentReference = "#/components/messages/" + JsonPointer.EscapeToken(message.Key);
componentMessages.Add(message.Key, message.Value);
componentMessageReferences.Add(message.Key, new AsyncApiMessageReference(componentReference));
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private static AsyncApiSecuritySchemeReference LoadSecuritySchemeByReference(
ParsingContext context,
string schemeName)
{
var securitySchemeObject = new AsyncApiSecuritySchemeReference("#/components/securitySchemes/" + schemeName);
var securitySchemeObject = new AsyncApiSecuritySchemeReference("#/components/securitySchemes/" + JsonPointer.EscapeToken(schemeName));

return securitySchemeObject;
}
Expand Down
46 changes: 23 additions & 23 deletions src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,141 +44,141 @@ public void RegisterComponents(AsyncApiDocument document)
// Register Schema
foreach (var item in document.Components.Schemas)
{
location = componentsBaseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Schema.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Parameters
foreach (var item in document.Components.Parameters)
{
location = componentsBaseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Parameter.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Channels
foreach (var item in document.Components.Channels)
{
location = componentsBaseUri + ReferenceType.Channel.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Channel.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Operations
foreach (var item in document.Components.Operations)
{
location = componentsBaseUri + ReferenceType.Operation.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Operation.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Servers
foreach (var item in document.Components.Servers)
{
location = componentsBaseUri + ReferenceType.Server.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Server.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register ServerVariables
foreach (var item in document.Components.ServerVariables)
{
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Messages
foreach (var item in document.Components.Messages)
{
location = componentsBaseUri + ReferenceType.Message.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Message.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register SecuritySchemes
foreach (var item in document.Components.SecuritySchemes)
{
location = componentsBaseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
this.RegisterComponent(item.Key, item.Value);
}

// Register Server Variables
foreach (var item in document.Components.ServerVariables)
{
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register CorrelationIds
foreach (var item in document.Components.CorrelationIds)
{
location = componentsBaseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Replies
foreach (var item in document.Components.Replies)
{
location = componentsBaseUri + ReferenceType.OperationReply.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.OperationReply.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register ReplyAddresses
foreach (var item in document.Components.ReplyAddresses)
{
location = componentsBaseUri + ReferenceType.OperationReplyAddress.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.OperationReplyAddress.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register ExternalDocs
foreach (var item in document.Components.ExternalDocs)
{
location = componentsBaseUri + ReferenceType.ExternalDocs.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.ExternalDocs.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register Tags
foreach (var item in document.Components.Tags)
{
location = componentsBaseUri + ReferenceType.Tag.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.Tag.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register OperationTraits
foreach (var item in document.Components.OperationTraits)
{
location = componentsBaseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register MessageTraits
foreach (var item in document.Components.MessageTraits)
{
location = componentsBaseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register ServerBindings
foreach (var item in document.Components.ServerBindings)
{
location = componentsBaseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register ChannelBindings
foreach (var item in document.Components.ChannelBindings)
{
location = componentsBaseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register OperationBindings
foreach (var item in document.Components.OperationBindings)
{
location = componentsBaseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

// Register MessageBindings
foreach (var item in document.Components.MessageBindings)
{
location = componentsBaseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + item.Key;
location = componentsBaseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
this.RegisterComponent(location, item.Value);
}

Expand All @@ -196,12 +196,12 @@ public void RegisterComponents(AsyncApiDocument document)
registerableChannelValue = this.ResolveReference<AsyncApiChannel>(reference.Reference);
}

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

foreach (var message in registerableChannelValue.Messages)
{
this.RegisterComponent(location + "/messages/" + message.Key, message.Value);
this.RegisterComponent(location + "/messages/" + JsonPointer.EscapeToken(message.Key), message.Value);
}
}

Expand All @@ -219,7 +219,7 @@ public void RegisterComponents(AsyncApiDocument document)
registerableServerValue = this.ResolveReference<AsyncApiServer>(serverReference.Reference);
}

location = serverBaseUri + server.Key;
location = serverBaseUri + JsonPointer.EscapeToken(server.Key);
this.RegisterComponent(location, registerableServerValue);
}
}
Expand Down
36 changes: 35 additions & 1 deletion src/ByteBard.AsyncAPI/JsonPointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,46 @@ public static JsonPointer FromUriFragment(string fragment)
return new JsonPointer(Uri.UnescapeDataString(fragment));
}

/// <summary>
/// Tries to create a pointer from its URI fragment identifier representation (RFC 6901 section 6).
/// </summary>
/// <param name="fragment">The fragment, without the leading <c>#</c>.</param>
/// <param name="pointer">The pointer, or <c>null</c> when the fragment is not a valid JSON pointer.</param>
/// <returns><c>true</c> when the fragment is a valid JSON pointer.</returns>
public static bool TryFromUriFragment(string fragment, out JsonPointer pointer)
{
pointer = null;
if (fragment == null)
{
return false;
}

try
{
pointer = FromUriFragment(fragment);
return true;
}
catch (AsyncApiException)
{
return false;
}
}

/// <summary>
/// Escapes a single reference token, so "~" becomes "~0" and "/" becomes "~1".
/// </summary>
/// <param name="token">The unescaped token.</param>
public static string EscapeToken(string token)
{
return token.Replace("~", "~0").Replace("/", "~1");
}

/// <summary>
/// Gets the JSON string representation of this JSON pointer.
/// </summary>
public override string ToString()
{
return string.Concat(this.Tokens.Select(token => "/" + token.Replace("~", "~0").Replace("/", "~1")));
return string.Concat(this.Tokens.Select(token => "/" + EscapeToken(token)));
}

private static string Unescape(string token, string pointer)
Expand Down
20 changes: 19 additions & 1 deletion src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/// <summary>
/// An optional string representation of this channel's address. The address is typically the "topic name", "routing key", "event type", or "path". When null or absent, it MUST be interpreted as unknown. This is useful when the address is generated dynamically at runtime or can't be known upfront. It MAY contain Channel Address Expressions. Query parameters and fragments SHALL NOT be used, instead use bindings to define them.
/// </summary>
public virtual string? Address { get; set; }

Check warning on line 18 in src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs

View workflow job for this annotation

GitHub Actions / build (macos-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 18 in src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

/// <summary>
/// A map of the messages that will be sent to this channel by any application at any time. Every message sent to this channel MUST be valid against one, and only one, of the message objects defined in this map.
Expand Down Expand Up @@ -84,7 +84,7 @@
writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description);

// servers
writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(s => s.Reference.FragmentId).ToList(), (w, s) => w.WriteValue(s));
writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(GetV2ServerName).ToList(), (w, s) => w.WriteValue(s));

var operations = writer.Workspace.RootDocument?.Operations.Values.Where(operation => CheckOperationChannel(operation, writer)).ToList();

Expand Down Expand Up @@ -166,5 +166,23 @@
operation.Channel.Reference.Workspace = writer.Workspace;
return operation.Channel.Equals(this);
}

/// <summary>
/// AsyncAPI 2.x lists channel servers by name, so a local "#/servers/{name}" pointer is written as "{name}".
/// Any other reference is written as is, since reducing it to a name would point it at a local server instead.
/// </summary>
private static string GetV2ServerName(AsyncApiServerReference server)
{
var reference = server.Reference;
if (!reference.IsExternal
&& JsonPointer.TryFromUriFragment(reference.FragmentId, out var pointer)
&& pointer.Tokens.Length == 2
&& pointer.Tokens[0] == AsyncApiConstants.Servers)
{
return pointer.Tokens[1];
}

return reference.Reference;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ public void SerializeAsSecurityRequirement(IAsyncApiWriter writer)
this.Reference.Workspace = writer.Workspace;

writer.WriteStartObject();
writer.WritePropertyName(this.Reference.FragmentId.Split('/').Last());
// 2.x security requirements list schemes by name: the unescaped last pointer token, or the bare name.
var fragmentId = this.Reference.FragmentId;
writer.WritePropertyName(JsonPointer.TryFromUriFragment(fragmentId, out var pointer) && pointer.Tokens.Length > 0 ? pointer.Tokens.Last() : fragmentId);
writer.WriteStartArray();

if (this.Scopes.Any())
Expand Down
10 changes: 10 additions & 0 deletions test/ByteBard.AsyncAPI.Tests/JsonPointer_Should.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ public void StringRepresentation_IsNotPercentDecoded()
new JsonPointer("/a%20b").Find(json).Should().BeNull();
}

[TestCase("plain", "plain")]
[TestCase("a/b", "a~1b")]
[TestCase("a~b", "a~0b")]
[TestCase("~1", "~01")]
public void EscapeToken_EscapesTildeBeforeSlash(string token, string expected)
{
JsonPointer.EscapeToken(token).Should().Be(expected);
new JsonPointer("/" + expected).Tokens.Should().Equal(token);
}

[Test]
public void Unescape_DecodesTilde1BeforeTilde0()
{
Expand Down
Loading
Loading