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.")]