Skip to content

Commit b61e78f

Browse files
bieluclaude
andcommitted
fix: parse 2.x channel server references with the strict JsonPointer
Removes the lenient JsonPointer.UnescapeToken. The 2.x writer now parses the reference fragment with JsonPointer, so a name is only written for an exact /servers/{name} pointer. Nested or malformed pointers (for example a '~' not followed by '0' or '1') are written unchanged, like other non-server references. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 95326dd commit b61e78f

3 files changed

Lines changed: 17 additions & 14 deletions

File tree

‎src/ByteBard.AsyncAPI/JsonPointer.cs‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,6 @@ public static string EscapeToken(string token)
9292
return token.Replace("~", "~0").Replace("/", "~1");
9393
}
9494

95-
/// <summary>
96-
/// Unescapes a single reference token, so "~1" becomes "/" and "~0" becomes "~".
97-
/// Unlike parsing a pointer, this does not reject a "~" that is not part of an escape sequence.
98-
/// </summary>
99-
/// <param name="token">The escaped token.</param>
100-
public static string UnescapeToken(string token)
101-
{
102-
return token.Replace("~1", "/").Replace("~0", "~");
103-
}
104-
10595
/// <summary>
10696
/// Gets the JSON string representation of this JSON pointer.
10797
/// </summary>

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Linq;
6+
using ByteBard.AsyncAPI.Exceptions;
67
using ByteBard.AsyncAPI.Extensions;
78
using ByteBard.AsyncAPI.Models.Interfaces;
89
using ByteBard.AsyncAPI.Writers;
@@ -173,15 +174,24 @@ private bool CheckOperationChannel(AsyncApiOperation operation, IAsyncApiWriter
173174
/// </summary>
174175
private static string GetV2ServerName(AsyncApiServerReference server)
175176
{
176-
const string serversPrefix = "/" + AsyncApiConstants.Servers + "/";
177177
var reference = server.Reference;
178-
if (reference.IsExternal || reference.FragmentId?.StartsWith(serversPrefix) != true)
178+
if (reference.IsExternal || reference.FragmentId == null)
179179
{
180180
return reference.Reference;
181181
}
182182

183-
// 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 JsonPointer.UnescapeToken(reference.FragmentId.Substring(serversPrefix.Length));
183+
string[] tokens;
184+
try
185+
{
186+
tokens = new JsonPointer(reference.FragmentId).Tokens;
187+
}
188+
catch (AsyncApiException)
189+
{
190+
// A malformed pointer names no server, so it is kept as is like any other non-server reference.
191+
return reference.Reference;
192+
}
193+
194+
return tokens.Length == 2 && tokens[0] == AsyncApiConstants.Servers ? tokens[1] : reference.Reference;
185195
}
186196
}
187197
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ public void V2_AsyncApiChannel_WithServerReference_SerializesServerName()
208208
[TestCase("#/servers/a~0b", "a~b")]
209209
[TestCase("other.yaml#/servers/production", "other.yaml#/servers/production")]
210210
[TestCase("#/components/servers/production", "'#/components/servers/production'")]
211+
[TestCase("#/servers/production/url", "'#/servers/production/url'")]
212+
[TestCase("#/servers/a~2b", "'#/servers/a~2b'")]
213+
[TestCase("#/servers/a~", "'#/servers/a~'")]
211214
public void V2_AsyncApiChannel_WithServerReference_SerializesServerNameOnlyForLocalServer(string reference, string expectedServer)
212215
{
213216
var channel = new AsyncApiChannel

0 commit comments

Comments
 (0)