Skip to content

Commit 127c75f

Browse files
authored
Merge pull request #22 from IntentArchitect/copilot/implement-aws-sqs-module
Implement AWS SQS module with metadata-driven configuration following IMPLEMENTATION_PLAN.md
2 parents 09655fc + 79333f8 commit 127c75f

File tree

11 files changed

+392
-20
lines changed

11 files changed

+392
-20
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using Intent.Metadata.Models;
3+
using Intent.Modelers.Eventing.Api;
4+
using Intent.Modules.Common;
5+
using Intent.RoslynWeaver.Attributes;
6+
7+
[assembly: DefaultIntentManaged(Mode.Fully)]
8+
[assembly: IntentTemplate("Intent.ModuleBuilder.Templates.Api.ApiElementModelExtensions", Version = "1.0")]
9+
10+
namespace Intent.Aws.Sqs.Api
11+
{
12+
public static class MessageModelStereotypeExtensions
13+
{
14+
public static AwsSqs GetAwsSqs(this MessageModel model)
15+
{
16+
var stereotype = model.GetStereotype(AwsSqs.DefinitionId);
17+
return stereotype != null ? new AwsSqs(stereotype) : null;
18+
}
19+
20+
public static bool HasAwsSqs(this MessageModel model)
21+
{
22+
return model.HasStereotype(AwsSqs.DefinitionId);
23+
}
24+
25+
public static bool TryGetAwsSqs(this MessageModel model, out AwsSqs stereotype)
26+
{
27+
if (!HasAwsSqs(model))
28+
{
29+
stereotype = null;
30+
return false;
31+
}
32+
33+
stereotype = new AwsSqs(model.GetStereotype(AwsSqs.DefinitionId));
34+
return true;
35+
}
36+
37+
public class AwsSqs
38+
{
39+
private IStereotype _stereotype;
40+
public const string DefinitionId = "f0b7e50e-71a9-4f31-9f9a-3c3e0b5d8f2e";
41+
42+
public AwsSqs(IStereotype stereotype)
43+
{
44+
_stereotype = stereotype;
45+
}
46+
47+
public string Name => _stereotype.Name;
48+
49+
public string QueueName()
50+
{
51+
return _stereotype.GetProperty<string>("Queue Name");
52+
}
53+
54+
public string QueueUrl()
55+
{
56+
return _stereotype.GetProperty<string>("Queue URL");
57+
}
58+
}
59+
}
60+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Intent.Engine;
2+
using Intent.Modules.Common;
3+
using Intent.Modules.Common.Plugins;
4+
using Intent.Modules.Integration.IaC.Shared.AwsSqs;
5+
using Intent.Plugins.FactoryExtensions;
6+
using Intent.RoslynWeaver.Attributes;
7+
8+
[assembly: DefaultIntentManaged(Mode.Fully)]
9+
[assembly: IntentTemplate("Intent.ModuleBuilder.Templates.FactoryExtension", Version = "1.0")]
10+
11+
namespace Intent.Modules.Aws.Sqs.FactoryExtensions
12+
{
13+
[IntentManaged(Mode.Fully, Body = Mode.Merge)]
14+
public class MetadataLoaderExtension : FactoryExtensionBase
15+
{
16+
public override string Id => "Intent.Aws.Sqs.MetadataLoaderExtension";
17+
18+
[IntentManaged(Mode.Ignore)]
19+
public override int Order => 0;
20+
21+
protected override void OnAfterMetadataLoad(IApplication application)
22+
{
23+
IntegrationManager.Initialize(application);
24+
}
25+
}
26+
}

Modules/Intent.Modules.Aws.Sqs/Intent.Aws.Sqs.imodspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@
5858
</template>
5959
</templates>
6060
<decorators></decorators>
61-
<factoryExtensions></factoryExtensions>
61+
<factoryExtensions>
62+
<factoryExtension id="Intent.Aws.Sqs.MetadataLoaderExtension" externalReference="9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d" />
63+
</factoryExtensions>
6264
<moduleSettings></moduleSettings>
6365
<dependencies>
6466
<dependency id="Intent.Common" version="3.9.0" />

Modules/Intent.Modules.Aws.Sqs/Intent.Modules.Aws.Sqs.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@
2020
<PackageReference Include="Intent.SoftwareFactory.SDK" Version="3.10.0-pre.2" />
2121
</ItemGroup>
2222

23+
<Import Project="..\Intent.Modules.Integration.IaC.Shared.AwsSqs\Intent.Modules.Integration.IaC.Shared.AwsSqs.projitems" Label="Shared" />
24+
2325
</Project>

Modules/Intent.Modules.Aws.Sqs/README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,28 @@ This follows the same pattern as the Azure Service Bus modules:
9898
- Intent.Modelers.Services (3.9.3+)
9999
- Intent.Modelers.Services.EventInteractions (1.2.1+)
100100

101+
## Metadata Support
102+
103+
### AWS SQS Stereotype
104+
105+
The module provides an `AWS SQS` stereotype that can be applied to message models in the Eventing designer:
106+
107+
- **Queue Name**: The name of the SQS queue (optional - defaults to kebab-case message name)
108+
- **Queue URL**: The full queue URL (optional - can be configured via appsettings)
109+
110+
### Metadata-Driven Configuration
111+
112+
The `SqsConfiguration` template uses the `IntegrationManager` to automatically:
113+
- Detect published and subscribed messages in your application
114+
- Generate appropriate publisher options (message type → queue URL mappings)
115+
- Generate subscription options (message type → handler mappings)
116+
- Register all event handlers in the DI container
117+
118+
No manual configuration is needed - the module generates everything based on your Intent Architect models.
119+
101120
## Future Enhancements
102121

103-
- Metadata-driven configuration (IntegrationManager integration)
104-
- AWS SQS stereotype for message models
105-
- Metadata loader extension
106-
- Bridge module for Lambda function generation
122+
- Bridge module for Lambda function generation (`Intent.AwsLambda.Sqs`)
107123

108124
## Related Modules
109125

Modules/Intent.Modules.Aws.Sqs/Templates/SqsConfiguration/SqsConfigurationTemplatePartial.cs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Intent.Modules.Common.Templates;
1616
using Intent.Modules.Constants;
1717
using Intent.Modules.Eventing.Contracts.Templates;
18+
using Intent.Modules.Integration.IaC.Shared.AwsSqs;
1819
using Intent.RoslynWeaver.Attributes;
1920
using Intent.Templates;
2021

@@ -83,22 +84,53 @@ public SqsConfigurationTemplate(IOutputTarget outputTarget, object model = null)
8384
method.AddStatement($"services.AddSingleton<{this.GetTypeName(SqsMessageDispatcherTemplate.TemplateId)}>();");
8485
method.AddStatement($"services.AddSingleton<{this.GetTypeName(SqsMessageDispatcherInterfaceTemplate.TemplateId)}, {this.GetTypeName(SqsMessageDispatcherTemplate.TemplateId)}>(sp => sp.GetRequiredService<{this.GetTypeName(SqsMessageDispatcherTemplate.TemplateId)}>());");
8586

86-
// Configure publisher options (metadata-driven - placeholder for now)
87-
method.AddStatement("");
88-
method.AddInvocationStatement($"services.Configure<{this.GetTypeName(SqsPublisherOptionsTemplate.TemplateId)}>", inv => inv
89-
.AddArgument(new CSharpLambdaBlock("options"), arg =>
90-
{
91-
arg.AddStatement("// Publisher options will be configured here based on metadata");
92-
arg.AddStatement("// Example: options.AddQueue<YourMessageType>(configuration[\"Sqs:Queues:YourMessageType:QueueUrl\"]!);");
93-
}));
87+
// Get metadata from IntegrationManager
88+
var publishers = IntegrationManager.Instance.GetAggregatedPublishedSqsItems(ExecutionContext.GetApplicationConfig().Id);
89+
var subscriptions = IntegrationManager.Instance.GetAggregatedSqsSubscriptions(ExecutionContext.GetApplicationConfig().Id);
90+
91+
// Configure publisher options (metadata-driven)
92+
if (publishers.Any())
93+
{
94+
method.AddStatement("");
95+
method.AddInvocationStatement($"services.Configure<{this.GetTypeName(SqsPublisherOptionsTemplate.TemplateId)}>", inv => inv
96+
.AddArgument(new CSharpLambdaBlock("options"), arg =>
97+
{
98+
foreach (var publisher in publishers)
99+
{
100+
var messageType = publisher.GetModelTypeName(this);
101+
var configKey = $"\"{publisher.QueueConfigurationName}:QueueUrl\"";
102+
arg.AddStatement($"options.AddQueue<{messageType}>(configuration[{configKey}]!);");
103+
}
104+
}));
105+
}
94106

95-
// Configure subscription options (metadata-driven - placeholder for now)
96-
method.AddInvocationStatement($"services.Configure<{this.GetTypeName(SqsSubscriptionOptionsTemplate.TemplateId)}>", inv => inv
97-
.AddArgument(new CSharpLambdaBlock("options"), arg =>
107+
// Register event handlers (metadata-driven)
108+
if (subscriptions.Any())
109+
{
110+
method.AddStatement("");
111+
foreach (var subscription in subscriptions)
98112
{
99-
arg.AddStatement("// Subscription options will be configured here based on metadata");
100-
arg.AddStatement("// Example: options.Add<YourMessageType, YourMessageHandler>();");
101-
}));
113+
var handlerType = subscription.SubscriptionItem.GetSubscriberTypeName(this);
114+
var handlerImplementation = this.GetTypeName("Intent.Eventing.Contracts.IntegrationEventHandler", subscription.EventHandlerModel);
115+
method.AddStatement($"services.AddTransient<{handlerType}, {handlerImplementation}>();");
116+
}
117+
}
118+
119+
// Configure subscription options (metadata-driven)
120+
if (subscriptions.Any())
121+
{
122+
method.AddStatement("");
123+
method.AddInvocationStatement($"services.Configure<{this.GetTypeName(SqsSubscriptionOptionsTemplate.TemplateId)}>", inv => inv
124+
.AddArgument(new CSharpLambdaBlock("options"), arg =>
125+
{
126+
foreach (var subscription in subscriptions)
127+
{
128+
var messageType = subscription.SubscriptionItem.GetModelTypeName(this);
129+
var handlerType = subscription.SubscriptionItem.GetSubscriberTypeName(this);
130+
arg.AddStatement($"options.Add<{messageType}, {handlerType}>();");
131+
}
132+
}));
133+
}
102134

103135
method.AddStatement("return services;", stmt => stmt.SeparatedFromPrevious());
104136
});
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Intent.Engine;
5+
using Intent.Modelers.Eventing.Api;
6+
using Intent.Modelers.Services.Api;
7+
using Intent.Modelers.Services.EventInteractions;
8+
9+
namespace Intent.Modules.Integration.IaC.Shared.AwsSqs;
10+
11+
internal class IntegrationManager
12+
{
13+
private static IntegrationManager? _instance;
14+
15+
public static void Initialize(IApplication application)
16+
{
17+
_instance = new IntegrationManager(application);
18+
}
19+
20+
public static IntegrationManager Instance
21+
{
22+
get
23+
{
24+
if (_instance is null)
25+
{
26+
throw new InvalidOperationException("AWS SQS Integration Manager not initialized.");
27+
}
28+
return _instance;
29+
}
30+
}
31+
32+
private readonly List<MessageInfo> _publishedMessages;
33+
private readonly List<MessageInfo> _subscribedMessages;
34+
35+
private IntegrationManager(IApplication application)
36+
{
37+
var applications = application.GetSolutionConfig()
38+
.GetApplicationReferences()
39+
.Select(app => application.GetSolutionConfig().GetApplicationConfig(app.Id))
40+
.ToArray();
41+
42+
const string awsSqsModule = "Intent.Aws.Sqs";
43+
44+
// Collect published messages
45+
_publishedMessages = applications
46+
.Where(app => app.Modules.Any(x => x.ModuleId == awsSqsModule))
47+
.SelectMany(app => application.MetadataManager
48+
.GetExplicitlyPublishedMessageModels(app.Id)
49+
.Select(message => new MessageInfo(app.Id, app.Name, message, null)))
50+
.Distinct()
51+
.ToList();
52+
53+
// Collect subscribed messages
54+
_subscribedMessages = applications
55+
.Where(app => app.Modules.Any(x => x.ModuleId == awsSqsModule))
56+
.SelectMany(app => application.MetadataManager
57+
.Services(app.Id)
58+
.GetIntegrationEventHandlerModels()
59+
.SelectMany(handler => handler.IntegrationEventSubscriptions()
60+
.Select(sub => new
61+
{
62+
Message = sub.Element.AsMessageModel(),
63+
Handler = handler
64+
}))
65+
.Select(sub => new MessageInfo(app.Id, app.Name, sub.Message, sub.Handler)))
66+
.Distinct()
67+
.ToList();
68+
}
69+
70+
public IReadOnlyList<SqsMessage> GetPublishedSqsMessages(string applicationId)
71+
{
72+
return _publishedMessages
73+
.Where(p => p.ApplicationId == applicationId)
74+
.Select(s => new SqsMessage(
75+
s.ApplicationId,
76+
s.ApplicationName,
77+
s.Message,
78+
SqsMethodType.Publish))
79+
.ToList();
80+
}
81+
82+
public IReadOnlyList<SqsMessage> GetSubscribedSqsMessages(string applicationId)
83+
{
84+
return _subscribedMessages
85+
.Where(p => p.ApplicationId == applicationId)
86+
.DistinctBy(s => s.Message.Id)
87+
.Select(s => new SqsMessage(
88+
s.ApplicationId,
89+
s.ApplicationName,
90+
s.Message,
91+
SqsMethodType.Subscribe))
92+
.ToList();
93+
}
94+
95+
public IReadOnlyList<SqsItemBase> GetAggregatedPublishedSqsItems(string applicationId)
96+
{
97+
return GetPublishedSqsMessages(applicationId)
98+
.Cast<SqsItemBase>()
99+
.ToList();
100+
}
101+
102+
public IReadOnlyList<SqsItemBase> GetAggregatedSubscribedSqsItems(string applicationId)
103+
{
104+
return GetSubscribedSqsMessages(applicationId)
105+
.Cast<SqsItemBase>()
106+
.ToList();
107+
}
108+
109+
public IReadOnlyList<SqsItemBase> GetAggregatedSqsItems(string applicationId)
110+
{
111+
var duplicateCheckSet = new HashSet<string>();
112+
return GetAggregatedPublishedSqsItems(applicationId)
113+
.Concat(GetAggregatedSubscribedSqsItems(applicationId))
114+
.Where(p => duplicateCheckSet.Add($"{p.QueueName}.{p.MethodType}"))
115+
.ToList();
116+
}
117+
118+
public IReadOnlyList<Subscription<SqsItemBase>> GetAggregatedSqsSubscriptions(string applicationId)
119+
{
120+
return _subscribedMessages
121+
.Where(message => message.ApplicationId == applicationId)
122+
.Select(message => new Subscription<SqsItemBase>(
123+
message.EventHandlerModel!,
124+
new SqsMessage(
125+
applicationId,
126+
message.ApplicationName,
127+
message.Message,
128+
SqsMethodType.Subscribe)))
129+
.ToList();
130+
}
131+
132+
public record Subscription<TSubscriptionItem>(
133+
IntegrationEventHandlerModel EventHandlerModel,
134+
TSubscriptionItem SubscriptionItem);
135+
136+
private record MessageInfo(
137+
string ApplicationId,
138+
string ApplicationName,
139+
MessageModel Message,
140+
IntegrationEventHandlerModel? EventHandlerModel);
141+
}

Modules/Intent.Modules.Integration.IaC.Shared.AwsSqs/Intent.Modules.Integration.IaC.Shared.AwsSqs.projitems

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<Import_RootNamespace>Intent.Modules.Integration.IaC.Shared.AwsSqs</Import_RootNamespace>
1010
</PropertyGroup>
1111
<ItemGroup>
12-
12+
<Compile Include="$(MSBuildThisFileDirectory)IntegrationManager.cs" />
13+
<Compile Include="$(MSBuildThisFileDirectory)SqsItemBase.cs" />
14+
<Compile Include="$(MSBuildThisFileDirectory)SqsMessage.cs" />
15+
<Compile Include="$(MSBuildThisFileDirectory)SqsMethodType.cs" />
1316
</ItemGroup>
1417
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Intent.Modules.Common.Templates;
2+
3+
namespace Intent.Modules.Integration.IaC.Shared.AwsSqs;
4+
5+
internal abstract record SqsItemBase
6+
{
7+
public string ApplicationId { get; init; }
8+
public string ApplicationName { get; init; }
9+
public SqsMethodType MethodType { get; init; }
10+
public string QueueName { get; init; }
11+
public string QueueConfigurationName { get; init; }
12+
13+
public abstract string GetModelTypeName(IntentTemplateBase template);
14+
public abstract string GetSubscriberTypeName<T>(IntentTemplateBase<T> template);
15+
}

0 commit comments

Comments
 (0)