Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 10 additions & 1 deletion src/ByteBard.AsyncAPI/JsonPointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,21 @@ public static JsonPointer FromUriFragment(string fragment)
return new JsonPointer(Uri.UnescapeDataString(fragment));
}

/// <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
29 changes: 28 additions & 1 deletion src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ByteBard.AsyncAPI.Exceptions;
using ByteBard.AsyncAPI.Extensions;
using ByteBard.AsyncAPI.Models.Interfaces;
using ByteBard.AsyncAPI.Writers;
Expand All @@ -15,7 +16,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 19 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.

/// <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 +85,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 +167,31 @@
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 || reference.FragmentId == null)
{
return reference.Reference;
}

string[] tokens;
try
{
tokens = new JsonPointer(reference.FragmentId).Tokens;
}
catch (AsyncApiException)
{
// A malformed pointer names no server, so it is kept as is like any other non-server reference.
return reference.Reference;
}

return tokens.Length == 2 && tokens[0] == AsyncApiConstants.Servers ? tokens[1] : reference.Reference;
}
}
}
206 changes: 206 additions & 0 deletions test/ByteBard.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Models.Interfaces;
using ByteBard.AsyncAPI.Readers;
using ByteBard.AsyncAPI.Validations;
using NUnit.Framework;

public class AsyncApiChannel_Should : TestBase
Expand Down Expand Up @@ -156,6 +157,211 @@ public void V2_AsyncApiChannel_UpgradesAndNormalizesKey()
reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
}

[Test]
public void V2_AsyncApiChannel_WithServers_SerializesServerNames()
{
var yaml =
"""
asyncapi: 2.6.0
info:
title: test spec
version: 1.0.0
servers:
production:
url: example.com
protocol: kafka
channels:
mychannel:
servers:
- production
""";

var document = new AsyncApiStringReader().Read(yaml, out var diag);

document.Channels.First().Value.Servers.Single().Reference.Reference.Should().Be("#/servers/production");

var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);

reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
}

[Test]
public void V2_AsyncApiChannel_WithServerReference_SerializesServerName()
{
var expected =
"""
servers:
- production
""";

var channel = new AsyncApiChannel
{
Servers = new List<AsyncApiServerReference> { new AsyncApiServerReference("#/servers/production") },
};

var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);

actual.Should().BePlatformAgnosticEquivalentTo(expected);
}

[TestCase("#/servers/east~1west", "east/west")]
[TestCase("#/servers/a~0b", "a~b")]
[TestCase("other.yaml#/servers/production", "other.yaml#/servers/production")]
[TestCase("#/components/servers/production", "'#/components/servers/production'")]
[TestCase("#/servers/production/url", "'#/servers/production/url'")]
[TestCase("#/servers/a~2b", "'#/servers/a~2b'")]
[TestCase("#/servers/a~", "'#/servers/a~'")]
public void V2_AsyncApiChannel_WithServerReference_SerializesServerNameOnlyForLocalServer(string reference, string expectedServer)
{
var channel = new AsyncApiChannel
{
Servers = new List<AsyncApiServerReference> { new AsyncApiServerReference(reference) },
};

var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);

actual.Should().BePlatformAgnosticEquivalentTo($"servers:\n - {expectedServer}");
}

[Test]
public void V3_AsyncApiChannel_WithServers_Reserializes()
{
var yaml =
"""
asyncapi: 3.1.0
info:
title: test spec
version: 1.0.0
servers:
production:
host: example.com
protocol: kafka
channels:
mychannel:
servers:
- $ref: '#/servers/production'
components: { }
""";

var document = new AsyncApiStringReader().Read(yaml, out var diag);

diag.Errors.Should().BeEmpty();
var server = document.Channels.First().Value.Servers.Single();
server.Reference.Reference.Should().Be("#/servers/production");
server.Host.Should().Be("example.com");

var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);

reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
}

[TestCase("a~1b", "#/servers/a~01b")]
[TestCase("a/b", "#/servers/a~1b")]
public void V2_AsyncApiChannel_WithServerNameNeedingEscape_Reserializes(string serverName, string expectedReference)
{
var yaml =
$"""
asyncapi: 2.6.0
info:
title: test spec
version: 1.0.0
servers:
{serverName}:
url: example.com
protocol: kafka
channels:
mychannel:
servers:
- {serverName}
""";

var document = new AsyncApiStringReader(new AsyncApiReaderSettings { RuleSet = ValidationRuleSet.GetEmptyRuleSet() }).Read(yaml, out var diag);

diag.Errors.Should().BeEmpty();
var server = document.Channels.First().Value.Servers.Single();
server.Reference.Reference.Should().Be(expectedReference);
server.Host.Should().Be("example.com");

var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);

reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
}

[Test]
public void V3_AsyncApiChannel_WithEscapedServerReference_ResolvesServer()
{
var yaml =
"""
asyncapi: 3.1.0
info:
title: test spec
version: 1.0.0
servers:
a/b:
host: example.com
protocol: kafka
channels:
mychannel:
servers:
- $ref: '#/servers/a~1b'
components: { }
""";

var document = new AsyncApiStringReader(new AsyncApiReaderSettings { RuleSet = ValidationRuleSet.GetEmptyRuleSet() }).Read(yaml, out var diag);

diag.Errors.Should().BeEmpty();
document.Channels.First().Value.Servers.Single().Host.Should().Be("example.com");

var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);

reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
}

[Test]
public void V3_AsyncApiChannel_WithServerReference_SerializesReference()
{
var expected =
"""
servers:
- $ref: '#/servers/production'
""";

var channel = new AsyncApiChannel
{
Servers = new List<AsyncApiServerReference> { new AsyncApiServerReference("#/servers/production") },
};

var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);

actual.Should().BePlatformAgnosticEquivalentTo(expected);
}

[Test]
public void V2_AsyncApiChannel_WithServers_UpgradesToV3References()
{
var yaml =
"""
asyncapi: 2.6.0
info:
title: test spec
version: 1.0.0
servers:
production:
url: example.com
protocol: kafka
channels:
mychannel:
servers:
- production
""";

var document = new AsyncApiStringReader().Read(yaml, out var diag);

var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);

reserialized.Should().Contain("- $ref: '#/servers/production'");
}

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