Skip to content

Commit 5e4ed51

Browse files
fix: add ApplicableVersions property to validation rules (#25)
* Add `ApplicableVersions` property to validation rules Added a new public property `AsyncApiVersion[] ApplicableVersions` in the base class `ValidationRule`. Updated existing rule classes (`OperationRequiredFields`, etc.) with `[AsyncApiVersionRule]` attributes. Modified `ValidationRuleSet` to set applicable versions for each rule based on custom attribute. * Update test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alex Wichmann <VisualBean@users.noreply.github.com> * fix coderabbit fubar * refactor validate to keep version --------- Signed-off-by: Alex Wichmann <VisualBean@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 1985b38 commit 5e4ed51

7 files changed

Lines changed: 176 additions & 5 deletions

File tree

src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class AsyncApiValidator : AsyncApiVisitorBase, IValidationContext
1414
private readonly ValidationRuleSet ruleSet;
1515
private readonly IList<AsyncApiValidatorError> errors = new List<AsyncApiValidatorError>();
1616
private readonly IList<AsyncApiValidatorWarning> warnings = new List<AsyncApiValidatorWarning>();
17+
private AsyncApiVersion? documentVersion;
1718

1819
/// <summary>
1920
/// Create a vistor that will validate an AsyncApiDocument.
@@ -23,6 +24,7 @@ public AsyncApiValidator(ValidationRuleSet ruleSet, AsyncApiDocument rootDocumen
2324
{
2425
this.ruleSet = ruleSet;
2526
this.RootDocument = rootDocument;
27+
this.documentVersion = this.GetDocumentVersion();
2628
}
2729

2830
public AsyncApiDocument RootDocument { get; }
@@ -198,11 +200,26 @@ private void Validate(object item, Type type)
198200
type = typeof(IAsyncApiReferenceable);
199201
}
200202

201-
var rules = this.ruleSet.FindRules(type);
203+
var rules = this.ruleSet.FindRules(type, this.documentVersion);
202204
foreach (var rule in rules)
203205
{
204206
rule.Evaluate(this as IValidationContext, item);
205207
}
206208
}
209+
210+
private AsyncApiVersion? GetDocumentVersion()
211+
{
212+
if (this.RootDocument?.Asyncapi?.StartsWith("2") == true)
213+
{
214+
return AsyncApiVersion.AsyncApi2_0;
215+
}
216+
217+
if (this.RootDocument?.Asyncapi?.StartsWith("3") == true)
218+
{
219+
return AsyncApiVersion.AsyncApi3_0;
220+
}
221+
222+
return null;
223+
}
207224
}
208225
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace ByteBard.AsyncAPI.Validation.Rules
1+
namespace ByteBard.AsyncAPI.Validation.Rules
22
{
33
using System;
44
using System.Collections.Generic;
@@ -75,6 +75,22 @@ public static class AsyncApiDocumentRules
7575
}
7676
}
7777

78+
context.Exit();
79+
});
80+
81+
[AsyncApiVersionRule(AsyncApiVersion.AsyncApi2_0)]
82+
public static ValidationRule<AsyncApiDocument> V2ChannelsRequired =>
83+
new ValidationRule<AsyncApiDocument>(
84+
(context, document) =>
85+
{
86+
context.Enter("channels");
87+
if (document.Channels == null || document.Channels.Count == 0)
88+
{
89+
context.CreateError(
90+
nameof(DocumentRequiredFields),
91+
string.Format(Resource.Validation_FieldRequired, "channels", "document"));
92+
}
93+
7894
context.Exit();
7995
});
8096
}

src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace ByteBard.AsyncAPI.Validation.Rules
1010
[AsyncApiRule]
1111
public static class AsyncApiOperationRules
1212
{
13+
[AsyncApiVersionRule(AsyncApiVersion.AsyncApi3_0)]
1314
public static ValidationRule<AsyncApiOperation> OperationRequiredFields =>
1415
new ValidationRule<AsyncApiOperation>(
1516
(context, operation) =>
@@ -35,6 +36,7 @@ public static class AsyncApiOperationRules
3536
context.Exit();
3637
});
3738

39+
[AsyncApiVersionRule(AsyncApiVersion.AsyncApi3_0)]
3840
public static ValidationRule<AsyncApiOperation> OperationChannelReference =>
3941
new ValidationRule<AsyncApiOperation>(
4042
(context, operation) =>
@@ -56,6 +58,7 @@ public static class AsyncApiOperationRules
5658
}
5759
});
5860

61+
[AsyncApiVersionRule(AsyncApiVersion.AsyncApi3_0)]
5962
public static ValidationRule<AsyncApiOperation> OperationMessages =>
6063
new ValidationRule<AsyncApiOperation>(
6164
(context, operation) =>

src/ByteBard.AsyncAPI/Validation/ValidationRule.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ public abstract class ValidationRule
1212
/// </summary>
1313
internal abstract Type ElementType { get; }
1414

15+
/// <summary>
16+
/// Gets or sets the AsyncAPI versions this rule applies to.
17+
/// Null means the rule applies to all versions.
18+
/// </summary>
19+
public AsyncApiVersion[] ApplicableVersions { get; internal set; }
20+
1521
/// <summary>
1622
/// Validate the object.
1723
/// </summary>

src/ByteBard.AsyncAPI/Validation/ValidationRuleSet.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,25 @@ public IList<ValidationRule> FindRules(Type type)
3030
return results ?? this.emptyRules;
3131
}
3232

33+
/// <summary>
34+
/// Retrieve the rules that are related to a specific type and version.
35+
/// </summary>
36+
/// <param name="type">The type that is to be validated.</param>
37+
/// <param name="version">The AsyncAPI version to filter rules by. If null, all rules are returned.</param>
38+
/// <returns>Either the rules related to the type and version, or an empty list.</returns>
39+
public IList<ValidationRule> FindRules(Type type, AsyncApiVersion? version)
40+
{
41+
var allRules = this.FindRules(type);
42+
if (version == null)
43+
{
44+
return allRules;
45+
}
46+
47+
return allRules.Where(r =>
48+
r.ApplicableVersions == null ||
49+
r.ApplicableVersions.Contains(version.Value)).ToList();
50+
}
51+
3352
/// <summary>
3453
/// Gets the default validation rule sets.
3554
/// </summary>
@@ -161,19 +180,25 @@ private static ValidationRuleSet BuildDefaultRuleSet()
161180
ValidationRuleSet ruleSet = new ValidationRuleSet();
162181
Type validationRuleType = typeof(ValidationRule);
163182

164-
IEnumerable<PropertyInfo> rules = typeof(ValidationRuleSet).Assembly.GetTypes()
183+
IEnumerable<PropertyInfo> ruleProperties = typeof(ValidationRuleSet).Assembly.GetTypes()
165184
.Where(t => t.IsClass
166185
&& t != typeof(object)
167186
&& t.GetCustomAttributes(typeof(AsyncApiRuleAttribute), false).Any())
168187
.SelectMany(t2 => t2.GetProperties(BindingFlags.Static | BindingFlags.Public)
169188
.Where(p => validationRuleType.IsAssignableFrom(p.PropertyType)));
170189

171-
foreach (var property in rules)
190+
foreach (var property in ruleProperties)
172191
{
173192
var propertyValue = property.GetValue(null); // static property
174193
ValidationRule rule = propertyValue as ValidationRule;
175194
if (rule != null)
176195
{
196+
var versionAttribute = property.GetCustomAttribute<AsyncApiVersionRuleAttribute>();
197+
if (versionAttribute != null)
198+
{
199+
rule.ApplicableVersions = versionAttribute.Versions;
200+
}
201+
177202
ruleSet.Add(rule);
178203
}
179204
}
@@ -186,4 +211,15 @@ private static ValidationRuleSet BuildDefaultRuleSet()
186211
public class AsyncApiRuleAttribute : Attribute
187212
{
188213
}
214+
215+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
216+
public class AsyncApiVersionRuleAttribute : Attribute
217+
{
218+
public AsyncApiVersion[] Versions { get; }
219+
220+
public AsyncApiVersionRuleAttribute(params AsyncApiVersion[] versions)
221+
{
222+
Versions = versions;
223+
}
224+
}
189225
}

test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,104 @@
22

33
using System.Linq;
44
using FluentAssertions;
5+
using ByteBard.AsyncAPI.Models;
56
using ByteBard.AsyncAPI.Readers;
7+
using ByteBard.AsyncAPI.Validations;
68
using NUnit.Framework;
79

810
public class ValidationRuleTests
911
{
12+
[Test]
13+
public void V2_DocumentWithNoChannels_ShouldError()
14+
{
15+
// arrange
16+
var input =
17+
"""
18+
asyncapi: 2.6.0
19+
info:
20+
title: Chat Application
21+
version: 1.0.0
22+
""";
23+
24+
// act
25+
new AsyncApiStringReader().Read(input, out var diagnostic);
26+
27+
// assert
28+
diagnostic.Errors.Should().Contain(e => e.Message == "The field 'channels' in 'document' object is REQUIRED.");
29+
}
30+
31+
[Test]
32+
public void V2_DocumentWithChannels_ShouldPass()
33+
{
34+
// arrange
35+
var input =
36+
"""
37+
asyncapi: 2.6.0
38+
info:
39+
title: Chat Application
40+
version: 1.0.0
41+
channels:
42+
chat:
43+
publish:
44+
operationId: onMessageReceived
45+
message:
46+
name: text
47+
payload:
48+
type: string
49+
""";
50+
51+
// act
52+
new AsyncApiStringReader().Read(input, out var diagnostic);
53+
54+
// assert
55+
diagnostic.Errors.Should().NotContain(e => e.Message.Contains("channels"));
56+
}
57+
58+
[Test]
59+
public void V3_DocumentWithNoChannels_ShouldPass()
60+
{
61+
// arrange
62+
var input =
63+
"""
64+
asyncapi: 3.0.0
65+
info:
66+
title: Chat Application
67+
version: 1.0.0
68+
""";
69+
70+
// act
71+
new AsyncApiStringReader().Read(input, out var diagnostic);
72+
73+
// assert
74+
diagnostic.Errors.Should().NotContain(e => e.Message.Contains("channels") && e.Message.Contains("REQUIRED"));
75+
}
76+
77+
[Test]
78+
public void VersionAwareRuleSet_V2Rule_DoesNotRunOnV3Document()
79+
{
80+
// arrange
81+
var ruleSet = ValidationRuleSet.GetDefaultRuleSet();
82+
83+
// act
84+
var rules = ruleSet.FindRules(typeof(AsyncApiDocument), AsyncApiVersion.AsyncApi3_0);
85+
86+
// assert
87+
rules.Should().NotContain(r => r.ApplicableVersions != null && r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi2_0) && !r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi3_0));
88+
}
89+
90+
[Test]
91+
public void VersionAwareRuleSet_V3Rule_DoesNotRunOnV2Document()
92+
{
93+
// arrange
94+
var ruleSet = ValidationRuleSet.GetDefaultRuleSet();
95+
96+
// act
97+
var rules = ruleSet.FindRules(typeof(AsyncApiOperation), AsyncApiVersion.AsyncApi2_0);
98+
99+
// assert
100+
rules.Should().NotContain(r => r.ApplicableVersions != null && r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi3_0) && !r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi2_0));
101+
}
102+
10103
[Test]
11104
public void V2_OperationId_WithNonUniqueKey_DiagnosticsError()
12105
{

test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void V2_DefaultRuleSet_PropertyReturnsTheCorrectRules()
3333
Assert.IsNotEmpty(rules);
3434

3535
// Update the number if you add new default rule(s).
36-
Assert.AreEqual(27, rules.Count);
36+
Assert.AreEqual(28, rules.Count);
3737
}
3838
}
3939
}

0 commit comments

Comments
 (0)