diff --git a/src/Saunter/Attributes/AsyncApiAttribute.cs b/src/Saunter/Attributes/AsyncApiAttribute.cs index a886966..b92b3ba 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 b7ea022..de8939d 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 d4301da..aab0c95 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 99315b4..77f2662 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 2e59b36..8fd8d2a 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 _, TenantUpdated __) { } public void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __) { } } + [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 _, TenantUpdated __) { } public void PublishTenantRemovedEvent(Guid _, TenantRemoved __) { } } + [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 _, TEvent __) } } + [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 _, AnyTenantCreated __) } } + [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 _, TenantUpdated __) { } [Message(typeof(TenantRemoved))] public void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __) { } } + + [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 0000000..a518389 --- /dev/null +++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Reflection; +using Saunter.AsyncApiSchema.v2; +using Saunter.Attributes; +using Saunter.Generation; +using Shouldly; +using Xunit; +using System.Linq; + +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 { } + } +}