Skip to content

Commit c7e570e

Browse files
bieluclaude
andcommitted
fix: keep non-local 2.x channel server references intact
Only a local "#/servers/{name}" reference is reduced to a server name. An external reference, or any other pointer, is written as is, since reducing it to its last token would silently point the channel at a local server of the same name. The name is decoded as one token, so both the 2.x reader's unescaped form and an escaped 3.x pointer give the same name. Also adds 3.x channel server tests, so the change is shown not to regress 3.x serialization or the 2.x to 3.x upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cdfc29d commit c7e570e

2 files changed

Lines changed: 101 additions & 6 deletions

File tree

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,20 @@ private bool CheckOperationChannel(AsyncApiOperation operation, IAsyncApiWriter
168168
}
169169

170170
/// <summary>
171-
/// AsyncAPI 2.x lists channel servers by name, so a "#/servers/{name}" pointer is written as "{name}".
171+
/// AsyncAPI 2.x lists channel servers by name, so a local "#/servers/{name}" pointer is written as "{name}".
172+
/// Any other reference is written as is, since reducing it to a name would point it at a local server instead.
172173
/// </summary>
173174
private static string GetV2ServerName(AsyncApiServerReference server)
174175
{
175-
var fragmentId = server.Reference.FragmentId;
176-
if (fragmentId == null)
176+
const string serversPrefix = "/" + AsyncApiConstants.Servers + "/";
177+
var reference = server.Reference;
178+
if (reference.IsExternal || reference.FragmentId?.StartsWith(serversPrefix) != true)
177179
{
178-
return server.Reference.Reference;
180+
return reference.Reference;
179181
}
180182

181-
var tokens = new JsonPointer(fragmentId).Tokens;
182-
return tokens.Length == 2 && tokens[0] == AsyncApiConstants.Servers ? tokens[1] : fragmentId;
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 reference.FragmentId.Substring(serversPrefix.Length).Replace("~1", "/").Replace("~0", "~");
183185
}
184186
}
185187
}

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,99 @@ public void V2_AsyncApiChannel_WithServerReference_SerializesServerName()
203203
actual.Should().BePlatformAgnosticEquivalentTo(expected);
204204
}
205205

206+
[TestCase("#/servers/east~1west", "east/west")]
207+
[TestCase("#/servers/a~0b", "a~b")]
208+
[TestCase("other.yaml#/servers/production", "other.yaml#/servers/production")]
209+
[TestCase("#/components/servers/production", "'#/components/servers/production'")]
210+
public void V2_AsyncApiChannel_WithServerReference_SerializesServerNameOnlyForLocalServer(string reference, string expectedServer)
211+
{
212+
var channel = new AsyncApiChannel
213+
{
214+
Servers = new List<AsyncApiServerReference> { new AsyncApiServerReference(reference) },
215+
};
216+
217+
var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0);
218+
219+
actual.Should().BePlatformAgnosticEquivalentTo($"servers:\n - {expectedServer}");
220+
}
221+
222+
[Test]
223+
public void V3_AsyncApiChannel_WithServers_Reserializes()
224+
{
225+
var yaml =
226+
"""
227+
asyncapi: 3.1.0
228+
info:
229+
title: test spec
230+
version: 1.0.0
231+
servers:
232+
production:
233+
host: example.com
234+
protocol: kafka
235+
channels:
236+
mychannel:
237+
servers:
238+
- $ref: '#/servers/production'
239+
components: { }
240+
""";
241+
242+
var document = new AsyncApiStringReader().Read(yaml, out var diag);
243+
244+
diag.Errors.Should().BeEmpty();
245+
var server = document.Channels.First().Value.Servers.Single();
246+
server.Reference.Reference.Should().Be("#/servers/production");
247+
server.Host.Should().Be("example.com");
248+
249+
var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);
250+
251+
reserialized.Should().BePlatformAgnosticEquivalentTo(yaml);
252+
}
253+
254+
[Test]
255+
public void V3_AsyncApiChannel_WithServerReference_SerializesReference()
256+
{
257+
var expected =
258+
"""
259+
servers:
260+
- $ref: '#/servers/production'
261+
""";
262+
263+
var channel = new AsyncApiChannel
264+
{
265+
Servers = new List<AsyncApiServerReference> { new AsyncApiServerReference("#/servers/production") },
266+
};
267+
268+
var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);
269+
270+
actual.Should().BePlatformAgnosticEquivalentTo(expected);
271+
}
272+
273+
[Test]
274+
public void V2_AsyncApiChannel_WithServers_UpgradesToV3References()
275+
{
276+
var yaml =
277+
"""
278+
asyncapi: 2.6.0
279+
info:
280+
title: test spec
281+
version: 1.0.0
282+
servers:
283+
production:
284+
url: example.com
285+
protocol: kafka
286+
channels:
287+
mychannel:
288+
servers:
289+
- production
290+
""";
291+
292+
var document = new AsyncApiStringReader().Read(yaml, out var diag);
293+
294+
var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0);
295+
296+
reserialized.Should().Contain("- $ref: '#/servers/production'");
297+
}
298+
206299
[Test]
207300
public void V2_AsyncApiChannel_reserializes()
208301
{

0 commit comments

Comments
 (0)