diff --git a/examples/StreetlightsAPI/API.cs b/examples/StreetlightsAPI/API.cs index 7e496ccd..89c11c85 100644 --- a/examples/StreetlightsAPI/API.cs +++ b/examples/StreetlightsAPI/API.cs @@ -90,7 +90,7 @@ public Streetlight Add([FromBody] Streetlight streetlight) /// /// Inform about environmental lighting conditions for a particular streetlight. /// - [Channel(PublishLightMeasuredTopic, Servers = new []{"webapi"})] + [Channel(PublishLightMeasuredTopic, Servers = new []{"webapi"}, XParams = new[] { "key1=value1", "key2=value2" })] [PublishOperation(typeof(LightMeasuredEvent), "Light")] [HttpPost] [Route(PublishLightMeasuredTopic)] diff --git a/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs b/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs index 2fc151af..a1baf731 100644 --- a/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs +++ b/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Saunter.AsyncApiSchema.v2.Bindings; namespace Saunter.AsyncApiSchema.v2 @@ -8,6 +11,7 @@ namespace Saunter.AsyncApiSchema.v2 /// /// Describes the operations available on a single channel. /// + [JsonConverter(typeof(XParamsConverter))] public class ChannelItem { /// @@ -53,6 +57,12 @@ public class ChannelItem [JsonProperty("servers", NullValueHandling = NullValueHandling.Ignore)] public List Servers { get; set; } = new List(); + /// + /// Specification Extensions. The extensions properties are implemented as patterned fields that are always prefixed by "x-" and must be format 'key=value' + /// + [JsonIgnore] + public string[] XParams { get; set; } + public bool ShouldSerializeParameters() { return Parameters != null && Parameters.Count > 0; @@ -63,4 +73,90 @@ public bool ShouldSerializeServers() return Servers != null && Servers.Count > 0; } } + + internal class XParamsConverter : JsonConverter + { + [ThreadStatic] + static bool cannotWrite; + + // Disables the converter in a thread-safe manner. + bool CannotWrite { get { return cannotWrite; } set { cannotWrite = value; } } + + public override bool CanWrite { get { return !CannotWrite; } } + public struct PushValue : IDisposable + { + Action setValue; + T oldValue; + + public PushValue(T value, Func getValue, Action setValue) + { + if (getValue == null || setValue == null) + throw new ArgumentNullException(); + this.setValue = setValue; + this.oldValue = getValue(); + setValue(value); + } + + #region IDisposable Members + + // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class. + public void Dispose() + { + if (setValue != null) + setValue(oldValue); + } + + #endregion + } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + // Disabling writing prevents infinite recursion. + using (new PushValue(true, () => CannotWrite, val => CannotWrite = val)) + { + var obj = JObject.FromObject(value, serializer); + + var ci = value as ChannelItem; + if (ci != null && ci.XParams != null && ci.XParams.Any()) + { + foreach (var xparam in ci.XParams) + { + if (!string.IsNullOrEmpty(xparam) && xparam.Count(x => x == '=') == 1) + { + var splitParam = xparam.Trim().Split('='); + if (splitParam.Length == 2) + { + var xParamNode = new JProperty($"x-{splitParam[0]}", new JValue(splitParam[1])); + obj.Add(xParamNode); + } + else + { + throw new FormatException($"XParams format is not correct. Use 'key=value'"); + } + } + else + { + throw new FormatException($"XParams format is not correct. Use 'key=value'"); + } + } + } + obj.WriteTo(writer); + } + } + + public override bool CanConvert(Type objectType) + { + return typeof(ChannelItem).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } } \ No newline at end of file diff --git a/src/Saunter/Attributes/ChannelAttribute.cs b/src/Saunter/Attributes/ChannelAttribute.cs index 246de376..44999d4c 100644 --- a/src/Saunter/Attributes/ChannelAttribute.cs +++ b/src/Saunter/Attributes/ChannelAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Saunter.Attributes { @@ -32,6 +33,12 @@ public class ChannelAttribute : Attribute /// public string[] Servers { get; set; } + /// + /// Specification Extensions. The extensions properties are implemented as patterned fields that are always prefixed by "x-" and must be format 'key=value' + /// + public string[] XParams { get; set; } + + public ChannelAttribute(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); diff --git a/src/Saunter/Generation/DocumentGenerator.cs b/src/Saunter/Generation/DocumentGenerator.cs index 6b381c1e..b8421810 100644 --- a/src/Saunter/Generation/DocumentGenerator.cs +++ b/src/Saunter/Generation/DocumentGenerator.cs @@ -71,7 +71,7 @@ private static IDictionary GenerateChannelsFromMethods(IEnu foreach (var mc in methodsWithChannelAttribute) { if (mc.Channel == null) continue; - + var channelItem = new ChannelItem { Description = mc.Channel.Description, @@ -80,7 +80,8 @@ private static IDictionary GenerateChannelsFromMethods(IEnu Subscribe = GenerateOperationFromMethod(mc.Method, schemaResolver, OperationType.Subscribe, options, jsonSchemaGenerator, serviceProvider), Bindings = mc.Channel.BindingsRef != null ? new ChannelBindingsReference(mc.Channel.BindingsRef) : null, Servers = mc.Channel.Servers?.ToList(), - }; + XParams = mc.Channel.XParams + }; channels.AddOrAppend(mc.Channel.Name, channelItem); var context = new ChannelItemFilterContext(mc.Method, schemaResolver, jsonSchemaGenerator, mc.Channel); @@ -122,6 +123,7 @@ private static IDictionary GenerateChannelsFromClasses(IEnu Subscribe = GenerateOperationFromClass(cc.Type, schemaResolver, OperationType.Subscribe, jsonSchemaGenerator), Bindings = cc.Channel.BindingsRef != null ? new ChannelBindingsReference(cc.Channel.BindingsRef) : null, Servers = cc.Channel.Servers?.ToList(), + XParams = cc.Channel.XParams }; channels.AddOrAppend(cc.Channel.Name, channelItem); diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs index 5df8628c..3dbb5e97 100644 --- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs +++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs @@ -18,7 +18,7 @@ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel() // Arrange var options = new AsyncApiOptions(); var documentGenerator = new DocumentGenerator(); - + // Act var document = documentGenerator.GenerateDocument(new[] { typeof(TenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); @@ -52,7 +52,7 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTh var documentGenerator = new DocumentGenerator(); // Act - var document = documentGenerator.GenerateDocument(new []{ typeof(TenantGenericMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + var document = documentGenerator.GenerateDocument(new[] { typeof(TenantGenericMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); // Assert document.ShouldNotBeNull(); @@ -69,7 +69,7 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTh var messages = publish.Message.ShouldBeOfType(); messages.OneOf.Count.ShouldBe(3); - + messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantCreated"); messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantUpdated"); messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantRemoved"); @@ -84,7 +84,7 @@ public void GenerateDocument_GeneratesDocumentWithSingleMessage() var documentGenerator = new DocumentGenerator(); // Act - var document = documentGenerator.GenerateDocument(new []{ typeof(TenantSingleMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + var document = documentGenerator.GenerateDocument(new[] { typeof(TenantSingleMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); // Assert document.ShouldNotBeNull(); @@ -93,7 +93,7 @@ public void GenerateDocument_GeneratesDocumentWithSingleMessage() var channel = document.Channels.First(); channel.Key.ShouldBe("asw.tenant_service.tenants_history"); channel.Value.Description.ShouldBe("Tenant events."); - + var publish = channel.Value.Publish; publish.ShouldNotBeNull(); publish.OperationId.ShouldBe("TenantSingleMessagePublisher"); @@ -161,7 +161,7 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters() var documentGenerator = new DocumentGenerator(); // Act - var document = documentGenerator.GenerateDocument(new []{ typeof(OneTenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + var document = documentGenerator.GenerateDocument(new[] { typeof(OneTenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); // Assert document.ShouldNotBeNull(); @@ -170,10 +170,12 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters() var channel = document.Channels.First(); channel.Key.ShouldBe("asw.tenant_service.{tenant_id}.{tenant_status}"); channel.Value.Description.ShouldBe("A tenant events."); + channel.Value.XParams.Length.ShouldBe(1); + channel.Value.XParams.First().ShouldBe("key=value"); channel.Value.Parameters.Count.ShouldBe(2); channel.Value.Parameters.Values.OfType().ShouldContain(p => p.Id == "tenant_id" && p.Value.Schema != null && p.Value.Description == "The tenant identifier."); channel.Value.Parameters.Values.OfType().ShouldContain(p => p.Id == "tenant_status" && p.Value.Schema != null && p.Value.Description == "The tenant status."); - + var subscribe = channel.Value.Subscribe; subscribe.ShouldNotBeNull(); subscribe.OperationId.ShouldBe("OneTenantMessageConsumer"); @@ -181,7 +183,7 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters() var messages = subscribe.Message.ShouldBeOfType(); messages.OneOf.Count.ShouldBe(3); - + messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantCreated"); messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantUpdated"); messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantRemoved"); @@ -197,7 +199,7 @@ public void GenerateDocument_GeneratesDocumentWithMessageHeader() // Act var document = documentGenerator.GenerateDocument(new[] { typeof(MyMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); - + // Assert document.ShouldNotBeNull(); @@ -272,7 +274,7 @@ public void PublishTenantCreated(Guid tenantId, AnyTenantCreated @event) } [AsyncApi] - [Channel("asw.tenant_service.{tenant_id}.{tenant_status}", Description = "A tenant events.")] + [Channel("asw.tenant_service.{tenant_id}.{tenant_status}", Description = "A tenant events.", XParams = new[] {"key=value"})] [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.")]