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)