Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/LEGO.AsyncAPI/Resource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/LEGO.AsyncAPI/Resource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Validation_ChannelsMustBeUnique" xml:space="preserve">
<value>Channel signature '{0}' MUST be unique.</value>
</data>
<data name="Validation_EmailMustBeEmailFormat" xml:space="preserve">
<value>The string '{0}' MUST be an email address.</value>
</data>
Expand Down
61 changes: 54 additions & 7 deletions src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace LEGO.AsyncAPI.Validation.Rules
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using LEGO.AsyncAPI.Models;
Expand All @@ -10,10 +12,13 @@ namespace LEGO.AsyncAPI.Validation.Rules
[AsyncApiRule]
public static class AsyncApiDocumentRules
{
private static TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);

/// <summary>
/// The key regex.
/// </summary>
public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$");
public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout);
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);

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

context.Enter("channels");
if (document.Channels == null || !document.Channels.Keys.Any())
try
{
context.CreateError(
nameof(DocumentRequiredFields),
string.Format(Resource.Validation_FieldRequired, "channels", "document"));
}
// MUST have at least 1 channel
if (document.Channels == null || !document.Channels.Keys.Any())
{
context.CreateError(
nameof(DocumentRequiredFields),
string.Format(Resource.Validation_FieldRequired, "channels", "document"));
return;
}
var hashSet = new HashSet<string>();
foreach (var key in document.Channels.Keys)
{
// Uri-template
if (!ChannelKeyUriTemplateRegex.IsMatch(key))
{
context.CreateError(
"ChannelKeys",
string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", KeyRegex.ToString()));
}

context.Exit();
// Unique channel keys
var pathSignature = GetKeySignature(key);
if (!hashSet.Add(pathSignature))
{
context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique, pathSignature));
}
}
}
finally
{
context.Exit();
}
});

private static string GetKeySignature(string path)
{
for (int openBrace = path.IndexOf('{'); openBrace > -1; openBrace = path.IndexOf('{', openBrace + 2))
{
int closeBrace = path.IndexOf('}', openBrace);

if (closeBrace < 0)
{
return path;
}

path = path.Substring(0, openBrace + 1) + path.Substring(closeBrace);
}

return path;
}

public static ValidationRule<AsyncApiDocument> KeyMustBeRegularExpression =>
new ValidationRule<AsyncApiDocument>(
(context, document) =>
Expand Down
126 changes: 126 additions & 0 deletions test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) The LEGO Group. All rights reserved.

namespace LEGO.AsyncAPI.Tests.Validation
{
using FluentAssertions;
using LEGO.AsyncAPI.Readers;
using LEGO.AsyncAPI.Validations;
using NUnit.Framework;
using System.Linq;

public class ValidationRuleTests
{
[Test]
[TestCase("chat-{person-id}")]
public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey)
{
var input =
$"""
asyncapi: 2.6.0
info:
title: Chat Application
version: 1.0.0
servers:
testing:
url: test.mosquitto.org:1883
protocol: mqtt
description: Test broker
channels:
{channelKey}:
publish:
operationId: onMessageReceieved
message:
name: text
payload:
type: string
subscribe:
operationId: sendMessage
message:
name: text
payload:
type: string
""";

var document = new AsyncApiStringReader().Read(input, out var diagnostic);
diagnostic.Errors.First().Message.Should().Be($"The key '{channelKey}' in 'channels' MUST match the regular expression '^[a-zA-Z0-9\\.\\-_]+$'.");
diagnostic.Errors.First().Pointer.Should().Be("#/channels");
}

[Test]
public void ChannelKey_WithNonUniqueKey_DiagnosticsError()
{
var input =
"""
asyncapi: 2.6.0
info:
title: Chat Application
version: 1.0.0
servers:
testing:
url: test.mosquitto.org:1883
protocol: mqtt
description: Test broker
channels:
chat/{personId}:
publish:
operationId: onMessageReceieved
message:
name: text
payload:
type: string
chat/{personIdentity}:
publish:
operationId: onMessageReceieved
message:
name: text
payload:
type: string
""";

var document = new AsyncApiStringReader().Read(input, out var diagnostic);
diagnostic.Errors.First().Message.Should().Be("Channel signature 'chat/{}' MUST be unique.");
diagnostic.Errors.First().Pointer.Should().Be("#/channels");
}

[Test]
[TestCase("chat")]
[TestCase("/some/chat/{personId}")]
[TestCase("chat-{personId}")]
[TestCase("chat-{person_id}")]
[TestCase("chat-{person%2Did}")]
[TestCase("chat-{personId2}")]
public void ChannelKey_WithValidKey_Success(string channelKey)
{
var input =
$"""
asyncapi: 2.6.0
info:
title: Chat Application
version: 1.0.0
servers:
testing:
url: test.mosquitto.org:1883
protocol: mqtt
description: Test broker
channels:
{channelKey}:
publish:
operationId: onMessageReceieved
message:
name: text
payload:
type: string
subscribe:
operationId: sendMessage
message:
name: text
payload:
type: string
""";

var document = new AsyncApiStringReader().Read(input, out var diagnostic);
diagnostic.Errors.Should().BeEmpty();
}
}

}
Loading