Skip to content

Commit 062ca41

Browse files
authored
Merge branch 'vnext' into fix/v3-external-multiformat-payload-reference
2 parents ed63267 + ff96a92 commit 062ca41

10 files changed

Lines changed: 401 additions & 32 deletions

‎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.Readers/V2/AsyncApiDocumentDeserializer.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ private static void SetOperations(ParsingContext context, AsyncApiDocument docum
136136
var correctChannelReference = document.Channels.FirstOrDefault(channel => channel.Value is AsyncApiChannelReference reference && reference.Reference.Reference.EndsWith(operationChannelFragmentKey));
137137
if (correctChannelReference.Key != null)
138138
{
139-
operation.Value.Channel = new AsyncApiChannelReference("#/channels/" + correctChannelReference.Key);
139+
operation.Value.Channel = new AsyncApiChannelReference("#/channels/" + JsonPointer.EscapeToken(correctChannelReference.Key));
140140
channel = correctChannelReference;
141141
}
142142
else
@@ -161,7 +161,7 @@ private static void SetOperations(ParsingContext context, AsyncApiDocument docum
161161
foreach (var message in messages)
162162
{
163163
channel.Value.Messages.TryAdd(message.Key, message.Value);
164-
operation.Value.Messages.Add(new AsyncApiMessageReference($"#/channels/{channel.Key}/messages/{message.Key}"));
164+
operation.Value.Messages.Add(new AsyncApiMessageReference($"#/channels/{JsonPointer.EscapeToken(channel.Key)}/messages/{JsonPointer.EscapeToken(message.Key)}"));
165165
}
166166
}
167167
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ private static KeyValuePair<string, AsyncApiMessage> GetMessage(ParseNode node)
4848
var message = LoadMessage(node);
4949
if (message is AsyncApiMessageReference reference)
5050
{
51-
key = reference.Reference.Reference.Split('/').Last();
51+
// Use the unescaped last pointer token, so the key matches the referenced component's key.
52+
key = reference.Reference.IsFragment && JsonPointer.TryFromUriFragment(reference.Reference.FragmentId, out var pointer) && pointer.Tokens.Length > 0
53+
? pointer.Tokens.Last()
54+
: reference.Reference.Reference.Split('/').Last();
5255
}
5356
else if (messageNode["messageId"] != null)
5457
{
@@ -87,7 +90,7 @@ private static void LoadMessages(ParseNode n, AsyncApiOperation instance)
8790
{
8891
if (message.Value is not AsyncApiMessageReference messageReference)
8992
{
90-
var componentReference = "#/components/messages/" + message.Key;
93+
var componentReference = "#/components/messages/" + JsonPointer.EscapeToken(message.Key);
9194
componentMessages.Add(message.Key, message.Value);
9295
componentMessageReferences.Add(message.Key, new AsyncApiMessageReference(componentReference));
9396
continue;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private static AsyncApiSecuritySchemeReference LoadSecuritySchemeByReference(
3838
ParsingContext context,
3939
string schemeName)
4040
{
41-
var securitySchemeObject = new AsyncApiSecuritySchemeReference("#/components/securitySchemes/" + schemeName);
41+
var securitySchemeObject = new AsyncApiSecuritySchemeReference("#/components/securitySchemes/" + JsonPointer.EscapeToken(schemeName));
4242

4343
return securitySchemeObject;
4444
}

‎src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs‎

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,141 +44,141 @@ public void RegisterComponents(AsyncApiDocument document)
4444
// Register Schema
4545
foreach (var item in document.Components.Schemas)
4646
{
47-
location = componentsBaseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key;
47+
location = componentsBaseUri + ReferenceType.Schema.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
4848
this.RegisterComponent(location, item.Value);
4949
}
5050

5151
// Register Parameters
5252
foreach (var item in document.Components.Parameters)
5353
{
54-
location = componentsBaseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key;
54+
location = componentsBaseUri + ReferenceType.Parameter.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
5555
this.RegisterComponent(location, item.Value);
5656
}
5757

5858
// Register Channels
5959
foreach (var item in document.Components.Channels)
6060
{
61-
location = componentsBaseUri + ReferenceType.Channel.GetDisplayName() + "/" + item.Key;
61+
location = componentsBaseUri + ReferenceType.Channel.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
6262
this.RegisterComponent(location, item.Value);
6363
}
6464

6565
// Register Operations
6666
foreach (var item in document.Components.Operations)
6767
{
68-
location = componentsBaseUri + ReferenceType.Operation.GetDisplayName() + "/" + item.Key;
68+
location = componentsBaseUri + ReferenceType.Operation.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
6969
this.RegisterComponent(location, item.Value);
7070
}
7171

7272
// Register Servers
7373
foreach (var item in document.Components.Servers)
7474
{
75-
location = componentsBaseUri + ReferenceType.Server.GetDisplayName() + "/" + item.Key;
75+
location = componentsBaseUri + ReferenceType.Server.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
7676
this.RegisterComponent(location, item.Value);
7777
}
7878

7979
// Register ServerVariables
8080
foreach (var item in document.Components.ServerVariables)
8181
{
82-
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key;
82+
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
8383
this.RegisterComponent(location, item.Value);
8484
}
8585

8686
// Register Messages
8787
foreach (var item in document.Components.Messages)
8888
{
89-
location = componentsBaseUri + ReferenceType.Message.GetDisplayName() + "/" + item.Key;
89+
location = componentsBaseUri + ReferenceType.Message.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
9090
this.RegisterComponent(location, item.Value);
9191
}
9292

9393
// Register SecuritySchemes
9494
foreach (var item in document.Components.SecuritySchemes)
9595
{
96-
location = componentsBaseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key;
96+
location = componentsBaseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
9797
this.RegisterComponent(location, item.Value);
9898
this.RegisterComponent(item.Key, item.Value);
9999
}
100100

101101
// Register Server Variables
102102
foreach (var item in document.Components.ServerVariables)
103103
{
104-
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key;
104+
location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
105105
this.RegisterComponent(location, item.Value);
106106
}
107107

108108
// Register CorrelationIds
109109
foreach (var item in document.Components.CorrelationIds)
110110
{
111-
location = componentsBaseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + item.Key;
111+
location = componentsBaseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
112112
this.RegisterComponent(location, item.Value);
113113
}
114114

115115
// Register Replies
116116
foreach (var item in document.Components.Replies)
117117
{
118-
location = componentsBaseUri + ReferenceType.OperationReply.GetDisplayName() + "/" + item.Key;
118+
location = componentsBaseUri + ReferenceType.OperationReply.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
119119
this.RegisterComponent(location, item.Value);
120120
}
121121

122122
// Register ReplyAddresses
123123
foreach (var item in document.Components.ReplyAddresses)
124124
{
125-
location = componentsBaseUri + ReferenceType.OperationReplyAddress.GetDisplayName() + "/" + item.Key;
125+
location = componentsBaseUri + ReferenceType.OperationReplyAddress.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
126126
this.RegisterComponent(location, item.Value);
127127
}
128128

129129
// Register ExternalDocs
130130
foreach (var item in document.Components.ExternalDocs)
131131
{
132-
location = componentsBaseUri + ReferenceType.ExternalDocs.GetDisplayName() + "/" + item.Key;
132+
location = componentsBaseUri + ReferenceType.ExternalDocs.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
133133
this.RegisterComponent(location, item.Value);
134134
}
135135

136136
// Register Tags
137137
foreach (var item in document.Components.Tags)
138138
{
139-
location = componentsBaseUri + ReferenceType.Tag.GetDisplayName() + "/" + item.Key;
139+
location = componentsBaseUri + ReferenceType.Tag.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
140140
this.RegisterComponent(location, item.Value);
141141
}
142142

143143
// Register OperationTraits
144144
foreach (var item in document.Components.OperationTraits)
145145
{
146-
location = componentsBaseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + item.Key;
146+
location = componentsBaseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
147147
this.RegisterComponent(location, item.Value);
148148
}
149149

150150
// Register MessageTraits
151151
foreach (var item in document.Components.MessageTraits)
152152
{
153-
location = componentsBaseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + item.Key;
153+
location = componentsBaseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
154154
this.RegisterComponent(location, item.Value);
155155
}
156156

157157
// Register ServerBindings
158158
foreach (var item in document.Components.ServerBindings)
159159
{
160-
location = componentsBaseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + item.Key;
160+
location = componentsBaseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
161161
this.RegisterComponent(location, item.Value);
162162
}
163163

164164
// Register ChannelBindings
165165
foreach (var item in document.Components.ChannelBindings)
166166
{
167-
location = componentsBaseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + item.Key;
167+
location = componentsBaseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
168168
this.RegisterComponent(location, item.Value);
169169
}
170170

171171
// Register OperationBindings
172172
foreach (var item in document.Components.OperationBindings)
173173
{
174-
location = componentsBaseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + item.Key;
174+
location = componentsBaseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
175175
this.RegisterComponent(location, item.Value);
176176
}
177177

178178
// Register MessageBindings
179179
foreach (var item in document.Components.MessageBindings)
180180
{
181-
location = componentsBaseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + item.Key;
181+
location = componentsBaseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + JsonPointer.EscapeToken(item.Key);
182182
this.RegisterComponent(location, item.Value);
183183
}
184184

@@ -196,12 +196,12 @@ public void RegisterComponents(AsyncApiDocument document)
196196
registerableChannelValue = this.ResolveReference<AsyncApiChannel>(reference.Reference);
197197
}
198198

199-
location = channelBaseUri + channel.Key;
199+
location = channelBaseUri + JsonPointer.EscapeToken(channel.Key);
200200
this.RegisterComponent(location, registerableChannelValue);
201201

202202
foreach (var message in registerableChannelValue.Messages)
203203
{
204-
this.RegisterComponent(location + "/messages/" + message.Key, message.Value);
204+
this.RegisterComponent(location + "/messages/" + JsonPointer.EscapeToken(message.Key), message.Value);
205205
}
206206
}
207207

@@ -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: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,46 @@ public static JsonPointer FromUriFragment(string fragment)
8383
return new JsonPointer(Uri.UnescapeDataString(fragment));
8484
}
8585

86+
/// <summary>
87+
/// Tries to create a pointer from its URI fragment identifier representation (RFC 6901 section 6).
88+
/// </summary>
89+
/// <param name="fragment">The fragment, without the leading <c>#</c>.</param>
90+
/// <param name="pointer">The pointer, or <c>null</c> when the fragment is not a valid JSON pointer.</param>
91+
/// <returns><c>true</c> when the fragment is a valid JSON pointer.</returns>
92+
public static bool TryFromUriFragment(string fragment, out JsonPointer pointer)
93+
{
94+
pointer = null;
95+
if (fragment == null)
96+
{
97+
return false;
98+
}
99+
100+
try
101+
{
102+
pointer = FromUriFragment(fragment);
103+
return true;
104+
}
105+
catch (AsyncApiException)
106+
{
107+
return false;
108+
}
109+
}
110+
111+
/// <summary>
112+
/// Escapes a single reference token, so "~" becomes "~0" and "/" becomes "~1".
113+
/// </summary>
114+
/// <param name="token">The unescaped token.</param>
115+
public static string EscapeToken(string token)
116+
{
117+
return token.Replace("~", "~0").Replace("/", "~1");
118+
}
119+
86120
/// <summary>
87121
/// Gets the JSON string representation of this JSON pointer.
88122
/// </summary>
89123
public override string ToString()
90124
{
91-
return string.Concat(this.Tokens.Select(token => "/" + token.Replace("~", "~0").Replace("/", "~1")));
125+
return string.Concat(this.Tokens.Select(token => "/" + EscapeToken(token)));
92126
}
93127

94128
private static string Unescape(string token, string pointer)

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public virtual void SerializeV2(IAsyncApiWriter writer)
8484
writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description);
8585

8686
// servers
87-
writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(s => s.Reference.FragmentId).ToList(), (w, s) => w.WriteValue(s));
87+
writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(GetV2ServerName).ToList(), (w, s) => w.WriteValue(s));
8888

8989
var operations = writer.Workspace.RootDocument?.Operations.Values.Where(operation => CheckOperationChannel(operation, writer)).ToList();
9090

@@ -166,5 +166,23 @@ private bool CheckOperationChannel(AsyncApiOperation operation, IAsyncApiWriter
166166
operation.Channel.Reference.Workspace = writer.Workspace;
167167
return operation.Channel.Equals(this);
168168
}
169+
170+
/// <summary>
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.
173+
/// </summary>
174+
private static string GetV2ServerName(AsyncApiServerReference server)
175+
{
176+
var reference = server.Reference;
177+
if (!reference.IsExternal
178+
&& JsonPointer.TryFromUriFragment(reference.FragmentId, out var pointer)
179+
&& pointer.Tokens.Length == 2
180+
&& pointer.Tokens[0] == AsyncApiConstants.Servers)
181+
{
182+
return pointer.Tokens[1];
183+
}
184+
185+
return reference.Reference;
186+
}
169187
}
170188
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ public void SerializeAsSecurityRequirement(IAsyncApiWriter writer)
8888
this.Reference.Workspace = writer.Workspace;
8989

9090
writer.WriteStartObject();
91-
writer.WritePropertyName(this.Reference.FragmentId.Split('/').Last());
91+
// 2.x security requirements list schemes by name: the unescaped last pointer token, or the bare name.
92+
var fragmentId = this.Reference.FragmentId;
93+
writer.WritePropertyName(JsonPointer.TryFromUriFragment(fragmentId, out var pointer) && pointer.Tokens.Length > 0 ? pointer.Tokens.Last() : fragmentId);
9294
writer.WriteStartArray();
9395

9496
if (this.Scopes.Any())

‎test/ByteBard.AsyncAPI.Tests/JsonPointer_Should.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ public void StringRepresentation_IsNotPercentDecoded()
7777
new JsonPointer("/a%20b").Find(json).Should().BeNull();
7878
}
7979

80+
[TestCase("plain", "plain")]
81+
[TestCase("a/b", "a~1b")]
82+
[TestCase("a~b", "a~0b")]
83+
[TestCase("~1", "~01")]
84+
public void EscapeToken_EscapesTildeBeforeSlash(string token, string expected)
85+
{
86+
JsonPointer.EscapeToken(token).Should().Be(expected);
87+
new JsonPointer("/" + expected).Tokens.Should().Equal(token);
88+
}
89+
8090
[Test]
8191
public void Unescape_DecodesTilde1BeforeTilde0()
8292
{

0 commit comments

Comments
 (0)