Skip to content

Commit 39a42b4

Browse files
bieluclaude
andcommitted
fix: escape 2.x channel server names in server references
The 2.x reader built "#/servers/{name}" without escaping the name, while the writer decodes the pointer token. A literal name such as "a~1b" therefore came back as "a/b" after a read/write round trip. The reader now escapes the name, and the workspace registers document servers under the escaped pointer so the reference still resolves. Server names this needs are outside the spec's key pattern, so this only matters when validation is off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8b92e76 commit 39a42b4

5 files changed

Lines changed: 85 additions & 4 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ internal static partial class AsyncApiV2Deserializer
2121
private static string GetServerReferenceKey(ValueNode valueNode)
2222
{
2323
var stringValue = valueNode.GetScalarValue();
24-
return stringValue.StartsWith("#/servers/") ? stringValue : "#/servers/" + stringValue;
24+
return stringValue.StartsWith("#/servers/") ? stringValue : "#/servers/" + JsonPointer.EscapeToken(stringValue);
2525
}
2626

2727
private static readonly PatternFieldMap<AsyncApiChannel> ChannelPatternFields =

‎src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public void RegisterComponents(AsyncApiDocument document)
219219
registerableServerValue = this.ResolveReference<AsyncApiServer>(serverReference.Reference);
220220
}
221221

222-
location = serverBaseUri + server.Key;
222+
location = serverBaseUri + JsonPointer.EscapeToken(server.Key);
223223
this.RegisterComponent(location, registerableServerValue);
224224
}
225225
}

‎src/ByteBard.AsyncAPI/JsonPointer.cs‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,30 @@ public JsonPointer ParentPointer
4949
}
5050
}
5151

52+
/// <summary>
53+
/// Escapes a single reference token, so "~" becomes "~0" and "/" becomes "~1".
54+
/// </summary>
55+
/// <param name="token">The unescaped token.</param>
56+
public static string EscapeToken(string token)
57+
{
58+
return token.Replace("~", "~0").Replace("/", "~1");
59+
}
60+
61+
/// <summary>
62+
/// Unescapes a single reference token, so "~1" becomes "/" and "~0" becomes "~".
63+
/// </summary>
64+
/// <param name="token">The escaped token.</param>
65+
public static string UnescapeToken(string token)
66+
{
67+
return token.Replace("~1", "/").Replace("~0", "~");
68+
}
69+
5270
/// <summary>
5371
/// Decode the string.
5472
/// </summary>
5573
private string Decode(string token)
5674
{
57-
return Uri.UnescapeDataString(token).Replace("~1", "/").Replace("~0", "~");
75+
return UnescapeToken(Uri.UnescapeDataString(token));
5876
}
5977

6078
/// <summary>

‎src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ private static string GetV2ServerName(AsyncApiServerReference server)
181181
}
182182

183183
// The 2.x reader keeps the name unescaped, while a 3.x pointer escapes it, so decode the whole remainder as one token.
184-
return reference.FragmentId.Substring(serversPrefix.Length).Replace("~1", "/").Replace("~0", "~");
184+
return JsonPointer.UnescapeToken(reference.FragmentId.Substring(serversPrefix.Length));
185185
}
186186
}
187187
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using ByteBard.AsyncAPI.Models;
99
using ByteBard.AsyncAPI.Models.Interfaces;
1010
using ByteBard.AsyncAPI.Readers;
11+
using ByteBard.AsyncAPI.Validations;
1112
using NUnit.Framework;
1213

1314
public class AsyncApiChannel_Should : TestBase
@@ -251,6 +252,68 @@ public void V3_AsyncApiChannel_WithServers_Reserializes()
251252
reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
252253
}
253254

255+
[TestCase("a~1b", "#/servers/a~01b")]
256+
[TestCase("a/b", "#/servers/a~1b")]
257+
public void V2_AsyncApiChannel_WithServerNameNeedingEscape_Reserializes(string serverName, string expectedReference)
258+
{
259+
var yaml =
260+
$"""
261+
asyncapi: 2.6.0
262+
info:
263+
title: test spec
264+
version: 1.0.0
265+
servers:
266+
{serverName}:
267+
url: example.com
268+
protocol: kafka
269+
channels:
270+
mychannel:
271+
servers:
272+
- {serverName}
273+
""";
274+
275+
var document = new AsyncApiStringReader(new AsyncApiReaderSettings { RuleSet = ValidationRuleSet.GetEmptyRuleSet() }).Read(yaml, out var diag);
276+
277+
diag.Errors.Should().BeEmpty();
278+
var server = document.Channels.First().Value.Servers.Single();
279+
server.Reference.Reference.Should().Be(expectedReference);
280+
server.Host.Should().Be("example.com");
281+
282+
var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);
283+
284+
reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
285+
}
286+
287+
[Test]
288+
public void V3_AsyncApiChannel_WithEscapedServerReference_ResolvesServer()
289+
{
290+
var yaml =
291+
"""
292+
asyncapi: 3.1.0
293+
info:
294+
title: test spec
295+
version: 1.0.0
296+
servers:
297+
a/b:
298+
host: example.com
299+
protocol: kafka
300+
channels:
301+
mychannel:
302+
servers:
303+
- $ref: '#/servers/a~1b'
304+
components: { }
305+
""";
306+
307+
var document = new AsyncApiStringReader(new AsyncApiReaderSettings { RuleSet = ValidationRuleSet.GetEmptyRuleSet() }).Read(yaml, out var diag);
308+
309+
diag.Errors.Should().BeEmpty();
310+
document.Channels.First().Value.Servers.Single().Host.Should().Be("example.com");
311+
312+
var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);
313+
314+
reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
315+
}
316+
254317
[Test]
255318
public void V3_AsyncApiChannel_WithServerReference_SerializesReference()
256319
{

0 commit comments

Comments
 (0)