Skip to content

Commit 04725c8

Browse files
Add Handlebars template output format to EventGrid reaction with per-query configuration (drasi-project#343)
* Initial plan * Add template format support to EventGrid reaction Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * Update template format to use per-query configuration Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * Add e2e integration tests for EventGrid template reaction Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * Remove custom reaction provider from test, use default EventGrid provider Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * Fix Event Grid Emulator image name to resolve timeout issues Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * Fix query config YAML formatting to use pipe syntax Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * Change template config from string to object with template and metadata fields Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> * load image for e2e tests Signed-off-by: Daniel Gerlag <daniel@gerlag.ca> * fix test Signed-off-by: Daniel Gerlag <daniel@gerlag.ca> * e2e test Signed-off-by: Daniel Gerlag <daniel@gerlag.ca> * Remove unstable eventgrid e2e tests Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --------- Signed-off-by: Daniel Gerlag <daniel@gerlag.ca> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> Co-authored-by: Daniel Gerlag <daniel@gerlag.ca>
1 parent 88c74e5 commit 04725c8

File tree

8 files changed

+237
-7
lines changed

8 files changed

+237
-7
lines changed

cli/installers/resources/default-reaction-providers.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ spec:
7777
enum:
7878
- "packed"
7979
- "unpacked"
80+
- "template"
8081
default: "packed"
8182
required:
8283
- eventGridUri

e2e-tests/fixtures/infrastructure.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const images = [
4242
"drasi-project/reaction-sync-dapr-statestore",
4343
"drasi-project/reaction-post-dapr-pubsub",
4444
"drasi-project/reaction-sync-vectorstore",
45+
"drasi-project/reaction-eventgrid",
4546
];
4647

4748
async function loadDrasiImages(clusterName, imageVersion = "latest") {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2024 The Drasi Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System.Text.Json.Serialization;
16+
17+
namespace Drasi.Reactions.EventGrid.Models
18+
{
19+
/// <summary>
20+
/// Configuration for a template with metadata for cloud events.
21+
/// </summary>
22+
public class TemplateConfig
23+
{
24+
/// <summary>
25+
/// The Handlebars template string for formatting the event data.
26+
/// </summary>
27+
[JsonPropertyName("template")]
28+
public string? Template { get; set; }
29+
30+
/// <summary>
31+
/// Key-value pairs for cloud event metadata/extension attributes.
32+
/// </summary>
33+
[JsonPropertyName("metadata")]
34+
public Dictionary<string, string>? Metadata { get; set; }
35+
}
36+
37+
/// <summary>
38+
/// Configuration for templates per query per change type.
39+
/// Used when format is set to "template".
40+
/// </summary>
41+
public class QueryConfig
42+
{
43+
/// <summary>
44+
/// Template configuration for formatting added results.
45+
/// </summary>
46+
[JsonPropertyName("added")]
47+
public TemplateConfig? Added { get; set; }
48+
49+
/// <summary>
50+
/// Template configuration for formatting updated results.
51+
/// </summary>
52+
[JsonPropertyName("updated")]
53+
public TemplateConfig? Updated { get; set; }
54+
55+
/// <summary>
56+
/// Template configuration for formatting deleted results.
57+
/// </summary>
58+
[JsonPropertyName("deleted")]
59+
public TemplateConfig? Deleted { get; set; }
60+
}
61+
}

reactions/azure/eventgrid-reaction/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
using Microsoft.Extensions.Logging;
2222

2323
using Drasi.Reactions.EventGrid.Services;
24+
using Drasi.Reactions.EventGrid.Models;
2425

25-
var reaction = new ReactionBuilder()
26+
var reaction = new ReactionBuilder<QueryConfig>()
2627
.UseChangeEventHandler<ChangeHandler>()
2728
.UseControlEventHandler<ControlSignalHandler>()
29+
.UseYamlQueryConfig()
2830
.ConfigureServices((services) => {
2931
services.AddSingleton<IChangeFormatter, ChangeFormatter>();
32+
services.AddSingleton<TemplateChangeFormatter>();
3033
services.AddSingleton<EventGridPublisherClient>(sp =>
3134
{
3235
var configuration = sp.GetRequiredService<IConfiguration>();

reactions/azure/eventgrid-reaction/Services/ChangeHandler.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,35 @@ namespace Drasi.Reactions.EventGrid.Services;
2020

2121
using Drasi.Reaction.SDK;
2222
using Drasi.Reaction.SDK.Models.QueryOutput;
23+
using Drasi.Reactions.EventGrid.Models;
2324
using Microsoft.Extensions.Configuration;
2425
using Microsoft.Extensions.Logging;
2526
using System.Text.Json;
2627
using Drasi.Reactions.EventGrid.Models.Unpacked;
2728

2829

2930

30-
public class ChangeHandler : IChangeEventHandler
31+
public class ChangeHandler : IChangeEventHandler<QueryConfig>
3132
{
3233
private readonly EventGridPublisherClient _publisherClient;
3334
private readonly OutputFormat _format;
3435
private readonly IChangeFormatter _formatter;
36+
private readonly TemplateChangeFormatter _templateFormatter;
3537
private readonly ILogger<ChangeHandler> _logger;
3638

3739
private readonly EventGridSchema _eventGridSchema;
3840

39-
public ChangeHandler(EventGridPublisherClient publisherClient,IConfiguration config, IChangeFormatter formatter, ILogger<ChangeHandler> logger)
41+
public ChangeHandler(EventGridPublisherClient publisherClient, IConfiguration config, IChangeFormatter formatter, TemplateChangeFormatter templateFormatter, ILogger<ChangeHandler> logger)
4042
{
4143
_publisherClient = publisherClient;
4244
_format = Enum.Parse<OutputFormat>(config.GetValue("format", "packed") ?? "packed", true);
4345
_formatter = formatter;
46+
_templateFormatter = templateFormatter;
4447
_logger = logger;
4548
_eventGridSchema = Enum.Parse<EventGridSchema>(config.GetValue<string>("eventGridSchema"));
4649
}
4750

48-
public async Task HandleChange(ChangeEvent evt, object? queryConfig)
51+
public async Task HandleChange(ChangeEvent evt, QueryConfig? queryConfig)
4952
{
5053
_logger.LogInformation("Processing " + evt.QueryId);
5154
switch(_format)
@@ -104,6 +107,47 @@ public async Task HandleChange(ChangeEvent evt, object? queryConfig)
104107
}
105108

106109
break;
110+
111+
case OutputFormat.Template:
112+
var templateResults = _templateFormatter.Format(evt, queryConfig);
113+
if (_eventGridSchema == EventGridSchema.EventGrid) {
114+
List<EventGridEvent> templateEvents = new List<EventGridEvent>();
115+
foreach (var templateResult in templateResults)
116+
{
117+
EventGridEvent templateEvent = new EventGridEvent(evt.QueryId, "Drasi.ChangeEvent", "1", templateResult.Data);
118+
templateEvents.Add(templateEvent);
119+
}
120+
var templateResp = await _publisherClient.SendEventsAsync(templateEvents);
121+
if (templateResp.IsError)
122+
{
123+
_logger.LogError($"Error sending message to Event Grid: {templateResp.Content.ToString()}");
124+
throw new Exception($"Error sending message to Event Grid: {templateResp.Content.ToString()}");
125+
}
126+
} else if (_eventGridSchema == EventGridSchema.CloudEvents) {
127+
List<CloudEvent> templateCloudEvents = new List<CloudEvent>();
128+
foreach (var templateResult in templateResults)
129+
{
130+
CloudEvent templateCloudEvent = new CloudEvent(evt.QueryId, "Drasi.ChangeEvent", templateResult.Data);
131+
132+
// Apply metadata as extension attributes if provided
133+
if (templateResult.Metadata != null)
134+
{
135+
foreach (var kvp in templateResult.Metadata)
136+
{
137+
templateCloudEvent.ExtensionAttributes[kvp.Key] = kvp.Value;
138+
}
139+
}
140+
141+
templateCloudEvents.Add(templateCloudEvent);
142+
}
143+
var templateCloudResp = await _publisherClient.SendEventsAsync(templateCloudEvents);
144+
if (templateCloudResp.IsError)
145+
{
146+
_logger.LogError($"Error sending message to Event Grid: {templateCloudResp.Content.ToString()}");
147+
throw new Exception($"Error sending message to Event Grid: {templateCloudResp.Content.ToString()}");
148+
}
149+
}
150+
break;
107151
default:
108152
throw new NotSupportedException("Invalid output format");
109153
}
@@ -114,7 +158,8 @@ public async Task HandleChange(ChangeEvent evt, object? queryConfig)
114158
enum OutputFormat
115159
{
116160
Packed,
117-
Unpacked
161+
Unpacked,
162+
Template
118163
}
119164

120165
enum EventGridSchema

reactions/azure/eventgrid-reaction/Services/ControlSignalHandler.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
namespace Drasi.Reactions.EventGrid.Services;
1616
using Drasi.Reaction.SDK;
1717
using Drasi.Reaction.SDK.Models.QueryOutput;
18+
using Drasi.Reactions.EventGrid.Models;
1819
using Azure.Messaging.EventGrid;
1920
using Azure.Messaging;
2021
// using Azure.Messaging.CloudEvents;
@@ -25,7 +26,7 @@ namespace Drasi.Reactions.EventGrid.Services;
2526
using Microsoft.Extensions.Logging;
2627
using Drasi.Reactions.EventGrid.Models.Unpacked;
2728

28-
public class ControlSignalHandler: IControlEventHandler
29+
public class ControlSignalHandler: IControlEventHandler<QueryConfig>
2930
{
3031
private readonly EventGridPublisherClient _publisherClient;
3132
private readonly OutputFormat _format;
@@ -42,7 +43,7 @@ public ControlSignalHandler(EventGridPublisherClient publisherClient, IConfigura
4243
_eventGridSchema = Enum.Parse<EventGridSchema>(config.GetValue<string>("eventGridSchema") ?? "CloudEvents", true);
4344
}
4445

45-
public async Task HandleControlSignal(ControlEvent evt, object? queryConfig)
46+
public async Task HandleControlSignal(ControlEvent evt, QueryConfig? queryConfig)
4647
{
4748
switch (_format)
4849
{
@@ -86,6 +87,7 @@ public async Task HandleControlSignal(ControlEvent evt, object? queryConfig)
8687
}
8788
break;
8889
case OutputFormat.Unpacked:
90+
case OutputFormat.Template:
8991
var notification = new ControlSignalNotification
9092
{
9193
Op = ControlSignalNotificationOp.X,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2024 The Drasi Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Drasi.Reaction.SDK.Models.QueryOutput;
16+
using Drasi.Reactions.EventGrid.Models;
17+
using HandlebarsDotNet;
18+
using System.Text.Json;
19+
20+
namespace Drasi.Reactions.EventGrid.Services
21+
{
22+
/// <summary>
23+
/// Represents a formatted template result with data and optional metadata.
24+
/// </summary>
25+
public class TemplateResult
26+
{
27+
public JsonElement Data { get; set; }
28+
public Dictionary<string, string>? Metadata { get; set; }
29+
}
30+
31+
public class TemplateChangeFormatter
32+
{
33+
private readonly IHandlebars _handlebars;
34+
35+
public TemplateChangeFormatter()
36+
{
37+
_handlebars = Handlebars.Create();
38+
}
39+
40+
public IEnumerable<TemplateResult> Format(ChangeEvent evt, QueryConfig? queryConfig)
41+
{
42+
if (queryConfig == null)
43+
{
44+
return Enumerable.Empty<TemplateResult>();
45+
}
46+
47+
var result = new List<TemplateResult>();
48+
49+
// Process added results
50+
if (queryConfig.Added != null && !string.IsNullOrEmpty(queryConfig.Added.Template))
51+
{
52+
var addedTemplate = _handlebars.Compile(queryConfig.Added.Template);
53+
foreach (var added in evt.AddedResults)
54+
{
55+
var templateData = new
56+
{
57+
after = added
58+
};
59+
60+
var output = addedTemplate(templateData);
61+
using var doc = JsonDocument.Parse(output);
62+
result.Add(new TemplateResult
63+
{
64+
Data = doc.RootElement.Clone(),
65+
Metadata = queryConfig.Added.Metadata
66+
});
67+
}
68+
}
69+
70+
// Process updated results
71+
if (queryConfig.Updated != null && !string.IsNullOrEmpty(queryConfig.Updated.Template))
72+
{
73+
var updatedTemplate = _handlebars.Compile(queryConfig.Updated.Template);
74+
foreach (var updated in evt.UpdatedResults)
75+
{
76+
var templateData = new
77+
{
78+
before = updated.Before,
79+
after = updated.After
80+
};
81+
82+
var output = updatedTemplate(templateData);
83+
using var doc = JsonDocument.Parse(output);
84+
result.Add(new TemplateResult
85+
{
86+
Data = doc.RootElement.Clone(),
87+
Metadata = queryConfig.Updated.Metadata
88+
});
89+
}
90+
}
91+
92+
// Process deleted results
93+
if (queryConfig.Deleted != null && !string.IsNullOrEmpty(queryConfig.Deleted.Template))
94+
{
95+
var deletedTemplate = _handlebars.Compile(queryConfig.Deleted.Template);
96+
foreach (var deleted in evt.DeletedResults)
97+
{
98+
var templateData = new
99+
{
100+
before = deleted
101+
};
102+
103+
var output = deletedTemplate(templateData);
104+
using var doc = JsonDocument.Parse(output);
105+
result.Add(new TemplateResult
106+
{
107+
Data = doc.RootElement.Clone(),
108+
Metadata = queryConfig.Deleted.Metadata
109+
});
110+
}
111+
}
112+
113+
return result;
114+
}
115+
}
116+
}

reactions/azure/eventgrid-reaction/eventgrid-reaction.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.28.0" />
1414
<PackageReference Include="CloudNative.CloudEvents" Version="2.8.0" />
1515
<PackageReference Include="Drasi.Reaction.SDK" Version="0.1.11-alpha" />
16+
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
1617
<PackageReference Include="Microsoft.Azure.Messaging.EventGrid.CloudNativeCloudEvents" Version="1.0.0" />
1718
</ItemGroup>
1819

0 commit comments

Comments
 (0)