Skip to content

Commit 6f8d6f7

Browse files
authored
chore: add channelKey validation rule (#202)
1 parent 3c4861d commit 6f8d6f7

File tree

4 files changed

+192
-7
lines changed

4 files changed

+192
-7
lines changed

src/LEGO.AsyncAPI/Resource.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/LEGO.AsyncAPI/Resource.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@
117117
<resheader name="writer">
118118
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119119
</resheader>
120+
<data name="Validation_ChannelsMustBeUnique" xml:space="preserve">
121+
<value>Channel signature '{0}' MUST be unique.</value>
122+
</data>
120123
<data name="Validation_EmailMustBeEmailFormat" xml:space="preserve">
121124
<value>The string '{0}' MUST be an email address.</value>
122125
</data>

src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace LEGO.AsyncAPI.Validation.Rules
44
{
5+
using System;
6+
using System.Collections.Generic;
57
using System.Linq;
68
using System.Text.RegularExpressions;
79
using LEGO.AsyncAPI.Models;
@@ -10,10 +12,13 @@ namespace LEGO.AsyncAPI.Validation.Rules
1012
[AsyncApiRule]
1113
public static class AsyncApiDocumentRules
1214
{
15+
private static TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
16+
1317
/// <summary>
1418
/// The key regex.
1519
/// </summary>
16-
public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$");
20+
public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout);
21+
public static Regex ChannelKeyUriTemplateRegex = new Regex(@"^(?:(?:[^\x00-\x20""'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$", RegexOptions.IgnoreCase, RegexTimeout);
1722

1823
public static ValidationRule<AsyncApiDocument> DocumentRequiredFields =>
1924
new ValidationRule<AsyncApiDocument>(
@@ -30,16 +35,58 @@ public static class AsyncApiDocumentRules
3035
context.Exit();
3136

3237
context.Enter("channels");
33-
if (document.Channels == null || !document.Channels.Keys.Any())
38+
try
3439
{
35-
context.CreateError(
36-
nameof(DocumentRequiredFields),
37-
string.Format(Resource.Validation_FieldRequired, "channels", "document"));
38-
}
40+
// MUST have at least 1 channel
41+
if (document.Channels == null || !document.Channels.Keys.Any())
42+
{
43+
context.CreateError(
44+
nameof(DocumentRequiredFields),
45+
string.Format(Resource.Validation_FieldRequired, "channels", "document"));
46+
return;
47+
}
48+
var hashSet = new HashSet<string>();
49+
foreach (var key in document.Channels.Keys)
50+
{
51+
// Uri-template
52+
if (!ChannelKeyUriTemplateRegex.IsMatch(key))
53+
{
54+
context.CreateError(
55+
"ChannelKeys",
56+
string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", KeyRegex.ToString()));
57+
}
3958

40-
context.Exit();
59+
// Unique channel keys
60+
var pathSignature = GetKeySignature(key);
61+
if (!hashSet.Add(pathSignature))
62+
{
63+
context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique, pathSignature));
64+
}
65+
}
66+
}
67+
finally
68+
{
69+
context.Exit();
70+
}
4171
});
4272

73+
private static string GetKeySignature(string path)
74+
{
75+
for (int openBrace = path.IndexOf('{'); openBrace > -1; openBrace = path.IndexOf('{', openBrace + 2))
76+
{
77+
int closeBrace = path.IndexOf('}', openBrace);
78+
79+
if (closeBrace < 0)
80+
{
81+
return path;
82+
}
83+
84+
path = path.Substring(0, openBrace + 1) + path.Substring(closeBrace);
85+
}
86+
87+
return path;
88+
}
89+
4390
public static ValidationRule<AsyncApiDocument> KeyMustBeRegularExpression =>
4491
new ValidationRule<AsyncApiDocument>(
4592
(context, document) =>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) The LEGO Group. All rights reserved.
2+
3+
namespace LEGO.AsyncAPI.Tests.Validation
4+
{
5+
using FluentAssertions;
6+
using LEGO.AsyncAPI.Readers;
7+
using LEGO.AsyncAPI.Validations;
8+
using NUnit.Framework;
9+
using System.Linq;
10+
11+
public class ValidationRuleTests
12+
{
13+
[Test]
14+
[TestCase("chat-{person-id}")]
15+
public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey)
16+
{
17+
var input =
18+
$"""
19+
asyncapi: 2.6.0
20+
info:
21+
title: Chat Application
22+
version: 1.0.0
23+
servers:
24+
testing:
25+
url: test.mosquitto.org:1883
26+
protocol: mqtt
27+
description: Test broker
28+
channels:
29+
{channelKey}:
30+
publish:
31+
operationId: onMessageReceieved
32+
message:
33+
name: text
34+
payload:
35+
type: string
36+
subscribe:
37+
operationId: sendMessage
38+
message:
39+
name: text
40+
payload:
41+
type: string
42+
""";
43+
44+
var document = new AsyncApiStringReader().Read(input, out var diagnostic);
45+
diagnostic.Errors.First().Message.Should().Be($"The key '{channelKey}' in 'channels' MUST match the regular expression '^[a-zA-Z0-9\\.\\-_]+$'.");
46+
diagnostic.Errors.First().Pointer.Should().Be("#/channels");
47+
}
48+
49+
[Test]
50+
public void ChannelKey_WithNonUniqueKey_DiagnosticsError()
51+
{
52+
var input =
53+
"""
54+
asyncapi: 2.6.0
55+
info:
56+
title: Chat Application
57+
version: 1.0.0
58+
servers:
59+
testing:
60+
url: test.mosquitto.org:1883
61+
protocol: mqtt
62+
description: Test broker
63+
channels:
64+
chat/{personId}:
65+
publish:
66+
operationId: onMessageReceieved
67+
message:
68+
name: text
69+
payload:
70+
type: string
71+
chat/{personIdentity}:
72+
publish:
73+
operationId: onMessageReceieved
74+
message:
75+
name: text
76+
payload:
77+
type: string
78+
""";
79+
80+
var document = new AsyncApiStringReader().Read(input, out var diagnostic);
81+
diagnostic.Errors.First().Message.Should().Be("Channel signature 'chat/{}' MUST be unique.");
82+
diagnostic.Errors.First().Pointer.Should().Be("#/channels");
83+
}
84+
85+
[Test]
86+
[TestCase("chat")]
87+
[TestCase("/some/chat/{personId}")]
88+
[TestCase("chat-{personId}")]
89+
[TestCase("chat-{person_id}")]
90+
[TestCase("chat-{person%2Did}")]
91+
[TestCase("chat-{personId2}")]
92+
public void ChannelKey_WithValidKey_Success(string channelKey)
93+
{
94+
var input =
95+
$"""
96+
asyncapi: 2.6.0
97+
info:
98+
title: Chat Application
99+
version: 1.0.0
100+
servers:
101+
testing:
102+
url: test.mosquitto.org:1883
103+
protocol: mqtt
104+
description: Test broker
105+
channels:
106+
{channelKey}:
107+
publish:
108+
operationId: onMessageReceieved
109+
message:
110+
name: text
111+
payload:
112+
type: string
113+
subscribe:
114+
operationId: sendMessage
115+
message:
116+
name: text
117+
payload:
118+
type: string
119+
""";
120+
121+
var document = new AsyncApiStringReader().Read(input, out var diagnostic);
122+
diagnostic.Errors.Should().BeEmpty();
123+
}
124+
}
125+
126+
}

0 commit comments

Comments
 (0)