Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/StreetlightsAPI/API.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public Streetlight Add([FromBody] Streetlight streetlight)
/// <summary>
/// Inform about environmental lighting conditions for a particular streetlight.
/// </summary>
[Channel(PublishLightMeasuredTopic, Servers = new []{"webapi"})]
[Channel(PublishLightMeasuredTopic, Servers = new []{"webapi"}, XParams = new[] { "key1=value1", "key2=value2" })]
[PublishOperation(typeof(LightMeasuredEvent), "Light")]
[HttpPost]
[Route(PublishLightMeasuredTopic)]
Expand Down
96 changes: 96 additions & 0 deletions src/Saunter/AsyncApiSchema/v2/ChannelItem.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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
{
/// <summary>
/// Describes the operations available on a single channel.
/// </summary>
[JsonConverter(typeof(XParamsConverter))]
public class ChannelItem
{
/// <summary>
Expand Down Expand Up @@ -53,6 +57,12 @@ public class ChannelItem
[JsonProperty("servers", NullValueHandling = NullValueHandling.Ignore)]
public List<string> Servers { get; set; } = new List<string>();

/// <summary>
/// Specification Extensions. The extensions properties are implemented as patterned fields that are always prefixed by "x-" and must be format 'key=value'
/// </summary>
[JsonIgnore]
public string[] XParams { get; set; }

public bool ShouldSerializeParameters()
{
return Parameters != null && Parameters.Count > 0;
Expand All @@ -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<T> : IDisposable
{
Action<T> setValue;
T oldValue;

public PushValue(T value, Func<T> getValue, Action<T> 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<bool>(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();
}
}
}
7 changes: 7 additions & 0 deletions src/Saunter/Attributes/ChannelAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace Saunter.Attributes
{
Expand Down Expand Up @@ -32,6 +33,12 @@ public class ChannelAttribute : Attribute
/// </summary>
public string[] Servers { get; set; }

/// <summary>
/// Specification Extensions. The extensions properties are implemented as patterned fields that are always prefixed by "x-" and must be format 'key=value'
/// </summary>
public string[] XParams { get; set; }


public ChannelAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Expand Down
6 changes: 4 additions & 2 deletions src/Saunter/Generation/DocumentGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private static IDictionary<string, ChannelItem> GenerateChannelsFromMethods(IEnu
foreach (var mc in methodsWithChannelAttribute)
{
if (mc.Channel == null) continue;

var channelItem = new ChannelItem
{
Description = mc.Channel.Description,
Expand All @@ -80,7 +80,8 @@ private static IDictionary<string, ChannelItem> 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);
Expand Down Expand Up @@ -122,6 +123,7 @@ private static IDictionary<string, ChannelItem> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand All @@ -69,7 +69,7 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTh

var messages = publish.Message.ShouldBeOfType<Messages>();
messages.OneOf.Count.ShouldBe(3);

messages.OneOf.OfType<MessageReference>().ShouldContain(m => m.Id == "anyTenantCreated");
messages.OneOf.OfType<MessageReference>().ShouldContain(m => m.Id == "anyTenantUpdated");
messages.OneOf.OfType<MessageReference>().ShouldContain(m => m.Id == "anyTenantRemoved");
Expand All @@ -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();
Expand All @@ -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");
Expand Down Expand Up @@ -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();
Expand All @@ -170,18 +170,20 @@ 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<ParameterReference>().ShouldContain(p => p.Id == "tenant_id" && p.Value.Schema != null && p.Value.Description == "The tenant identifier.");
channel.Value.Parameters.Values.OfType<ParameterReference>().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");
subscribe.Summary.ShouldBe("Subscribe to domains events about a tenant.");

var messages = subscribe.Message.ShouldBeOfType<Messages>();
messages.OneOf.Count.ShouldBe(3);

messages.OneOf.OfType<MessageReference>().ShouldContain(m => m.Id == "tenantCreated");
messages.OneOf.OfType<MessageReference>().ShouldContain(m => m.Id == "tenantUpdated");
messages.OneOf.OfType<MessageReference>().ShouldContain(m => m.Id == "tenantRemoved");
Expand All @@ -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();

Expand Down Expand Up @@ -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.")]
Expand Down