diff --git a/src/Saunter.UI/package-lock.json b/src/Saunter.UI/package-lock.json
index 65b5c972..ed3f11c4 100644
--- a/src/Saunter.UI/package-lock.json
+++ b/src/Saunter.UI/package-lock.json
@@ -2318,9 +2318,9 @@
}
},
"node_modules/ws": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
- "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"engines": {
"node": ">=8.3.0"
},
@@ -4105,9 +4105,9 @@
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
},
"ws": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz",
- "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"requires": {}
},
"xml-name-validator": {
diff --git a/src/Saunter/Attributes/AsyncApiAttribute.cs b/src/Saunter/Attributes/AsyncApiAttribute.cs
index a8869664..b92b3baf 100644
--- a/src/Saunter/Attributes/AsyncApiAttribute.cs
+++ b/src/Saunter/Attributes/AsyncApiAttribute.cs
@@ -3,9 +3,9 @@
namespace Saunter.Attributes
{
///
- /// Marks a class as containing asyncapi channels.
+ /// Marks a class or interface as containing asyncapi channels.
///
- [AttributeUsage(AttributeTargets.Class)]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public sealed class AsyncApiAttribute : Attribute
{
public string DocumentName { get; }
diff --git a/src/Saunter/Attributes/ChannelAttribute.cs b/src/Saunter/Attributes/ChannelAttribute.cs
index b7ea022a..de8939d8 100644
--- a/src/Saunter/Attributes/ChannelAttribute.cs
+++ b/src/Saunter/Attributes/ChannelAttribute.cs
@@ -2,7 +2,7 @@
namespace Saunter.Attributes
{
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)]
public class ChannelAttribute : Attribute
{
///
diff --git a/src/Saunter/Attributes/ChannelParameterAttribute.cs b/src/Saunter/Attributes/ChannelParameterAttribute.cs
index d4301da4..aab0c95a 100644
--- a/src/Saunter/Attributes/ChannelParameterAttribute.cs
+++ b/src/Saunter/Attributes/ChannelParameterAttribute.cs
@@ -2,7 +2,7 @@
namespace Saunter.Attributes
{
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
public class ChannelParameterAttribute : Attribute
{
public ChannelParameterAttribute(string name, Type type)
diff --git a/src/Saunter/Attributes/OperationAttribute.cs b/src/Saunter/Attributes/OperationAttribute.cs
index 99315b4b..77f26621 100644
--- a/src/Saunter/Attributes/OperationAttribute.cs
+++ b/src/Saunter/Attributes/OperationAttribute.cs
@@ -2,7 +2,7 @@
namespace Saunter.Attributes
{
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)]
public abstract class OperationAttribute : Attribute
{
public OperationType OperationType { get; protected set; }
diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs
index c6b90c79..c384d641 100644
--- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs
+++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs
@@ -12,15 +12,17 @@ namespace Saunter.Tests.Generation.DocumentGeneratorTests
{
public class ClassAttributesTests
{
- [Fact]
- public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel()
+ [Theory]
+ [InlineData(typeof(TenantMessageConsumer))]
+ [InlineData(typeof(ITenantMessageConsumer))]
+ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(TenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -44,15 +46,17 @@ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel()
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTheSameMethod()
+ [Theory]
+ [InlineData(typeof(TenantGenericMessagePublisher))]
+ [InlineData(typeof(ITenantGenericMessagePublisher))]
+ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTheSameMethod(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(TenantGenericMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -76,15 +80,17 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTh
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithSingleMessage()
+ [Theory]
+ [InlineData(typeof(TenantSingleMessagePublisher))]
+ [InlineData(typeof(ITenantSingleMessagePublisher))]
+ public void GenerateDocument_GeneratesDocumentWithSingleMessage(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(TenantSingleMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -104,8 +110,10 @@ public void GenerateDocument_GeneratesDocumentWithSingleMessage()
}
- [Fact]
- public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWithMultipleMessagesPerChannel()
+ [Theory]
+ [InlineData(typeof(TenantMessageConsumer), typeof(TenantMessagePublisher))]
+ [InlineData(typeof(ITenantMessageConsumer), typeof(ITenantMessagePublisher))]
+ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWithMultipleMessagesPerChannel(Type type1, Type type2)
{
// Arrange
var options = new AsyncApiOptions();
@@ -114,8 +122,8 @@ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWi
// Act
var document = documentGenerator.GenerateDocument(new[]
{
- typeof(TenantMessageConsumer).GetTypeInfo(),
- typeof(TenantMessagePublisher).GetTypeInfo()
+ type1.GetTypeInfo(),
+ type2.GetTypeInfo()
}, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
@@ -153,15 +161,17 @@ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWi
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithChannelParameters()
+ [Theory]
+ [InlineData(typeof(OneTenantMessageConsumer))]
+ [InlineData(typeof(IOneTenantMessageConsumer))]
+ public void GenerateDocument_GeneratesDocumentWithChannelParameters(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(OneTenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -188,15 +198,17 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters()
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithMessageHeader()
+ [Theory]
+ [InlineData(typeof(MyMessagePublisher))]
+ [InlineData(typeof(IMyMessagePublisher))]
+ public void GenerateDocument_GeneratesDocumentWithMessageHeader(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(MyMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -216,6 +228,15 @@ public class MyMessagePublisher
public void PublishMyMessage() { }
}
+ [AsyncApi]
+ [Channel("channel.my.message")]
+ [PublishOperation]
+ public interface IMyMessagePublisher
+ {
+ [Message(typeof(MyMessage), HeadersType = typeof(MyMessageHeader))]
+ void PublishMyMessage();
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[SubscribeOperation(OperationId = "TenantMessageConsumer", Summary = "Subscribe to domains events about tenants.")]
@@ -231,6 +252,21 @@ public void SubscribeTenantUpdatedEvent(Guid id, TenantUpdated updated) { }
public void SubscribeTenantRemovedEvent(Guid id, TenantRemoved removed) { }
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [SubscribeOperation(OperationId = "TenantMessageConsumer", Summary = "Subscribe to domains events about tenants.")]
+ public interface ITenantMessageConsumer
+ {
+ [Message(typeof(TenantCreated))]
+ void SubscribeTenantCreatedEvent(Guid _, TenantCreated __);
+
+ [Message(typeof(TenantUpdated))]
+ void SubscribeTenantUpdatedEvent(Guid _, TenantUpdated __);
+
+ [Message(typeof(TenantRemoved))]
+ void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __);
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
@@ -246,6 +282,21 @@ public void PublishTenantUpdatedEvent(Guid id, TenantUpdated updated) { }
public void PublishTenantRemovedEvent(Guid id, TenantRemoved removed) { }
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
+ public interface ITenantMessagePublisher
+ {
+ [Message(typeof(TenantCreated))]
+ void PublishTenantCreatedEvent(Guid _, TenantCreated __);
+
+ [Message(typeof(TenantUpdated))]
+ void PublishTenantUpdatedEvent(Guid _, TenantUpdated __);
+
+ [Message(typeof(TenantRemoved))]
+ void PublishTenantRemovedEvent(Guid _, TenantRemoved __);
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
@@ -260,6 +311,18 @@ public void PublishTenantEvent(Guid id, TEvent @event)
}
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
+ public interface ITenantGenericMessagePublisher
+ {
+ [Message(typeof(AnyTenantCreated))]
+ [Message(typeof(AnyTenantUpdated))]
+ [Message(typeof(AnyTenantRemoved))]
+ void PublishTenantEvent(Guid _, TEvent __)
+ where TEvent : IEvent;
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[PublishOperation(OperationId = "TenantSingleMessagePublisher", Summary = "Publish single domain event about tenants.")]
@@ -271,6 +334,15 @@ public void PublishTenantCreated(Guid id, AnyTenantCreated created)
}
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [PublishOperation(OperationId = "TenantSingleMessagePublisher", Summary = "Publish single domain event about tenants.")]
+ public interface ITenantSingleMessagePublisher
+ {
+ [Message(typeof(AnyTenantCreated))]
+ public void PublishTenantCreated(Guid _, AnyTenantCreated __);
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.{tenant_id}.{tenant_status}", Description = "A tenant events.")]
[ChannelParameter("tenant_id", typeof(long), Description = "The tenant identifier.")]
@@ -287,6 +359,23 @@ public void SubscribeTenantUpdatedEvent(Guid id, TenantUpdated updated) { }
[Message(typeof(TenantRemoved))]
public void SubscribeTenantRemovedEvent(Guid id, TenantRemoved removed) { }
}
+
+ [AsyncApi]
+ [Channel("asw.tenant_service.{tenant_id}.{tenant_status}", Description = "A tenant events.")]
+ [ChannelParameter("tenant_id", typeof(long), Description = "The tenant identifier.")]
+ [ChannelParameter("tenant_status", typeof(string), Description = "The tenant status.")]
+ [SubscribeOperation(OperationId = "OneTenantMessageConsumer", Summary = "Subscribe to domains events about a tenant.")]
+ public interface IOneTenantMessageConsumer
+ {
+ [Message(typeof(TenantCreated))]
+ void SubscribeTenantCreatedEvent(Guid _, TenantCreated __);
+
+ [Message(typeof(TenantUpdated))]
+ void SubscribeTenantUpdatedEvent(Guid _, TenantUpdated __);
+
+ [Message(typeof(TenantRemoved))]
+ void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __);
+ }
}
public class TenantCreated { }
diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs
new file mode 100644
index 00000000..f86f232e
--- /dev/null
+++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Saunter.AsyncApiSchema.v2;
+using Saunter.Attributes;
+using Saunter.Generation;
+using Shouldly;
+using Xunit;
+
+namespace Saunter.Tests.Generation.DocumentGeneratorTests
+{
+ public class InterfaceAttributeTests
+ {
+ [Theory]
+ [InlineData(typeof(IServiceEvents))]
+ [InlineData(typeof(ServiceEventsFromInterface))]
+ [InlineData(typeof(ServiceEventsFromAnnotatedInterface))] // Check that annotations are not inherited from the interface
+ public void NonAnnotatedTypesTest(Type type)
+ {
+ // Arrange
+ var options = new AsyncApiOptions();
+ var documentGenerator = new DocumentGenerator();
+
+ // Act
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+
+ // Assert
+ document.ShouldNotBeNull();
+ document.Channels.Count.ShouldBe(0);
+ }
+
+ [Theory]
+ [InlineData(typeof(IAnnotatedServiceEvents), "interface")]
+ [InlineData(typeof(AnnotatedServiceEventsFromInterface), "class")]
+ [InlineData(typeof(AnnotatedServiceEventsFromAnnotatedInterface), "class")] // Check that the actual type's annotation takes precedence of the inherited interface
+ public void AnnotatedTypesTest(Type type, string source)
+ {
+ // Arrange
+ var options = new AsyncApiOptions();
+ var documentGenerator = new DocumentGenerator();
+
+ // Act
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+
+ // Assert
+ document.ShouldNotBeNull();
+ document.Channels.Count.ShouldBe(1);
+
+ var channel = document.Channels.First();
+ channel.Key.ShouldBe($"{source}.event");
+ channel.Value.Description.ShouldBeNull();
+
+ var publish = channel.Value.Publish;
+ publish.ShouldNotBeNull();
+ publish.OperationId.ShouldBe("PublishEvent");
+ publish.Description.ShouldBe($"({source}) Subscribe to domains events about a tenant.");
+
+ var messageRef = publish.Message.ShouldBeOfType();
+ messageRef.Id.ShouldBe("tenantEvent");
+ }
+
+ [AsyncApi]
+ private interface IAnnotatedServiceEvents
+ {
+ [Channel("interface.event")]
+ [PublishOperation(typeof(TenantEvent), Description = "(interface) Subscribe to domains events about a tenant.")]
+ void PublishEvent(TenantEvent evt);
+ }
+
+ private interface IServiceEvents
+ {
+ void PublishEvent(TenantEvent evt);
+ }
+
+ private class ServiceEventsFromInterface : IServiceEvents
+ {
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ private class ServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents
+ {
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ [AsyncApi]
+ private class AnnotatedServiceEventsFromInterface : IAnnotatedServiceEvents
+ {
+ [Channel("class.event")]
+ [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")]
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ [AsyncApi]
+ private class AnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents
+ {
+ [Channel("class.event")]
+ [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")]
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ private class TenantEvent { }
+ }
+}
diff --git a/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs b/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs
index c3692640..8f3b4de2 100644
--- a/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs
+++ b/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs
@@ -32,7 +32,5 @@ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel()
document.ShouldNotBeNull();
}
}
-
-
}
}
diff --git a/test/Saunter.Tests/Generation/OperationTraitsTests.cs b/test/Saunter.Tests/Generation/OperationTraitsTests.cs
index 56ee526a..8b4e60a9 100644
--- a/test/Saunter.Tests/Generation/OperationTraitsTests.cs
+++ b/test/Saunter.Tests/Generation/OperationTraitsTests.cs
@@ -44,7 +44,6 @@ public void Example_OperationTraits()
}
}
-
private class TestOperationTraitsFilter : IOperationFilter
{
public void Apply(Operation publishOperation, OperationFilterContext context)