Skip to content

Commit e0a976b

Browse files
bieluclaude
andcommitted
fix: percent-encode 2.x channel references written by address
AsyncApiChannelReference.SerializeV2 writes a reference to a root channel with the channel's address, escaped with EscapeToken only. A channel addressed "a%20b" was written as "#/channels/a%20b", which decodes to "a b" and no longer resolves. It now uses EscapeUriFragmentToken, as does the 2.x reader when it points such a reference at the normalized channel key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 63fb61c commit e0a976b

3 files changed

Lines changed: 27 additions & 3 deletions

File tree

‎src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private static string NormalizeChannelPointer(string pointer)
6767
return pointer;
6868
}
6969

70-
return channelsPrefix + JsonPointer.EscapeToken(NormalizeChannelKey(parsed.Tokens[1]));
70+
return channelsPrefix + JsonPointer.EscapeUriFragmentToken(NormalizeChannelKey(parsed.Tokens[1]));
7171
}
7272

7373
public static string NormalizeChannelKey(string channelKey, ParseNode node = null)

‎src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public override void SerializeV2(IAsyncApiWriter writer)
6464
// 2.x keys root channels by address instead of by key. A target the document does not write
6565
// keeps its own reference, so it can be found dangling and written inline.
6666
writer.WriteStartObject();
67-
writer.WriteOptionalProperty(AsyncApiConstants.DollarRef, "#/channels/" + JsonPointer.EscapeToken(this.Target.Address));
67+
writer.WriteOptionalProperty(AsyncApiConstants.DollarRef, "#/channels/" + JsonPointer.EscapeUriFragmentToken(this.Target.Address));
6868
writer.WriteEndObject();
6969
return;
7070
}

‎test/ByteBard.AsyncAPI.Tests/Models/PercentEncodedReferenceKeys_Should.cs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,36 @@ public void V2_UpgradesToV3()
298298
upgraded.Operations["op"].Messages.Single().Equals(upgraded.Channels["usersignedup"].Messages["signed up"]).Should().BeTrue();
299299
}
300300

301+
// 2.x keys channels by address, so a reference to a root channel is written with its encoded address.
302+
[TestCase("a b", "a%20b")]
303+
[TestCase("a%20b", "a%2520b")]
304+
public void V2_ChannelReference_RoundTripsByEncodedAddress(string address, string encodedAddress)
305+
{
306+
var yaml =
307+
$"""
308+
asyncapi: 2.6.0
309+
info:
310+
title: test
311+
version: 1.0.0
312+
channels:
313+
{address}:
314+
description: target
315+
other:
316+
$ref: '#/channels/{encodedAddress}'
317+
""";
318+
319+
var actual = Read(yaml).SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);
320+
321+
actual.Should().BePlatformAgnosticEquivalentTo(yaml);
322+
Read(actual).Channels["other"].Description.Should().Be("target");
323+
}
324+
301325
private static AsyncApiDocument Read(string yaml)
302326
{
303327
var doc = new AsyncApiStringReader().Read(yaml, out var diagnostic);
304328

305329
// The keys fail the spec's key patterns; anything else means a reference did not resolve.
306-
diagnostic.Errors.Should().OnlyContain(e => e.Message.Contains("MUST match the regular expression"));
330+
diagnostic.Errors.Where(e => !e.Message.Contains("MUST match the regular expression")).Should().BeEmpty();
307331
return doc;
308332
}
309333
}

0 commit comments

Comments
 (0)