Skip to content
Merged
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
5 changes: 0 additions & 5 deletions src/AWS.Messaging/EventBridgeMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AWS.Messaging
{
Expand Down
343 changes: 174 additions & 169 deletions src/AWS.Messaging/Serialization/EnvelopeSerializer.cs

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions src/AWS.Messaging/Serialization/Handlers/MessageMetadataHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using Amazon.SQS.Model;
using AWS.Messaging.Internal;
using AWS.Messaging.Serialization.Helpers;
using MessageAttributeValue = Amazon.SimpleNotificationService.Model.MessageAttributeValue;

namespace AWS.Messaging.Serialization.Handlers;

/// <summary>
/// Handles the creation of metadata objects from various AWS messaging services.
/// </summary>
internal static class MessageMetadataHandler
{
/// <summary>
/// Creates SQS metadata from an SQS message.
/// </summary>
/// <param name="message">The SQS message containing metadata information.</param>
/// <returns>An SQSMetadata object containing the extracted metadata.</returns>
public static SQSMetadata CreateSQSMetadata(Message message)
{
var metadata = new SQSMetadata
{
MessageID = message.MessageId,
ReceiptHandle = message.ReceiptHandle,
MessageAttributes = message.MessageAttributes,
};

if (message.Attributes != null)
{
metadata.MessageGroupId = JsonPropertyHelper.GetAttributeValue(message.Attributes, "MessageGroupId");
metadata.MessageDeduplicationId = JsonPropertyHelper.GetAttributeValue(message.Attributes, "MessageDeduplicationId");
}

return metadata;
}

/// <summary>
/// Creates SNS metadata from a JSON element representing an SNS message.
/// </summary>
/// <param name="root">The root JSON element containing SNS metadata information.</param>
/// <returns>An SNSMetadata object containing the extracted metadata.</returns>
public static SNSMetadata CreateSNSMetadata(JsonElement root)
{
var metadata = new SNSMetadata
{
MessageId = JsonPropertyHelper.GetStringProperty(root, "MessageId"),
TopicArn = JsonPropertyHelper.GetStringProperty(root, "TopicArn"),
Timestamp = JsonPropertyHelper.GetDateTimeOffsetProperty(root, "Timestamp") ?? default,
UnsubscribeURL = JsonPropertyHelper.GetStringProperty(root, "UnsubscribeURL"),
Subject = JsonPropertyHelper.GetStringProperty(root, "Subject"),
};

if (root.TryGetProperty("MessageAttributes", out var messageAttributes))
{
metadata.MessageAttributes = messageAttributes.Deserialize(MessagingJsonSerializerContext.Default.DictionarySNSMessageAttributeValue);

}

return metadata;
}

/// <summary>
/// Creates EventBridge metadata from a JSON element representing an EventBridge event.
/// </summary>
/// <param name="root">The root JSON element containing EventBridge metadata information.</param>
/// <returns>An EventBridgeMetadata object containing the extracted metadata.</returns>
public static EventBridgeMetadata CreateEventBridgeMetadata(JsonElement root)
{
var metadata = new EventBridgeMetadata
{
EventId = JsonPropertyHelper.GetStringProperty(root, "id"),
DetailType = JsonPropertyHelper.GetStringProperty(root, "detail-type"),
Source = JsonPropertyHelper.GetStringProperty(root, "source"),
AWSAccount = JsonPropertyHelper.GetStringProperty(root, "account"),
Time = JsonPropertyHelper.GetDateTimeOffsetProperty(root, "time") ?? default,
AWSRegion = JsonPropertyHelper.GetStringProperty(root, "region"),
};

if (root.TryGetProperty("resources", out var resources))
{
metadata.Resources = resources.EnumerateArray()
.Select(x => x.GetString())
.Where(x => x != null)
.ToList()!;
}

return metadata;
}
}
94 changes: 94 additions & 0 deletions src/AWS.Messaging/Serialization/Helpers/JsonPropertyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;

namespace AWS.Messaging.Serialization.Helpers;

/// <summary>
/// Provides helper methods for safely extracting values from JsonElement and Dictionary objects.
/// </summary>
internal static class JsonPropertyHelper
{
/// <summary>
/// Safely extracts a value from a JsonElement using the provided conversion function.
/// </summary>
/// <typeparam name="T">The type to convert the property value to.</typeparam>
/// <param name="root">The root JsonElement containing the property.</param>
/// <param name="propertyName">The name of the property to extract.</param>
/// <param name="getValue">The function to convert the property value to type T.</param>
/// <returns>The converted value or default if the property doesn't exist.</returns>
public static T? GetPropertyValue<T>(JsonElement root, string propertyName, Func<JsonElement, T> getValue)
{
if (getValue == null)
{
throw new ArgumentNullException(nameof(getValue));
}


return root.TryGetProperty(propertyName, out var property) ? getValue(property) : default;
}

/// <summary>
/// Extracts a required value from a JsonElement using the provided conversion function.
/// </summary>
/// <typeparam name="T">The type to convert the property value to.</typeparam>
/// <param name="root">The root JsonElement containing the property.</param>
/// <param name="propertyName">The name of the property to extract.</param>
/// <param name="getValue">The function to convert the property value to type T.</param>
/// <returns>The converted value.</returns>
/// <exception cref="InvalidDataException">Thrown when the property is missing or conversion fails.</exception>
public static T GetRequiredProperty<T>(JsonElement root, string propertyName, Func<JsonElement, T> getValue)
{
if (root.TryGetProperty(propertyName, out var property))
{
try
{
return getValue(property);
}
catch (Exception ex)
{
throw new InvalidDataException($"Failed to get or convert property '{propertyName}'", ex);
}
}
throw new InvalidDataException($"Required property '{propertyName}' is missing");
}

/// <summary>
/// Safely extracts a string value from a JsonElement.
/// </summary>
/// <param name="root">The root JsonElement containing the property.</param>
/// <param name="propertyName">The name of the property to extract.</param>
/// <returns>The string value or null if the property doesn't exist.</returns>
public static string? GetStringProperty(JsonElement root, string propertyName)
=> GetPropertyValue(root, propertyName, element => element.GetString());

/// <summary>
/// Safely extracts a DateTimeOffset value from a JsonElement.
/// </summary>
/// <param name="root">The root JsonElement containing the property.</param>
/// <param name="propertyName">The name of the property to extract.</param>
/// <returns>The DateTimeOffset value or null if the property doesn't exist.</returns>
public static DateTimeOffset? GetDateTimeOffsetProperty(JsonElement root, string propertyName)
=> GetPropertyValue(root, propertyName, element => element.GetDateTimeOffset());

/// <summary>
/// Safely extracts a Uri value from a JsonElement.
/// </summary>
/// <param name="root">The root JsonElement containing the property.</param>
/// <param name="propertyName">The name of the property to extract.</param>
/// <returns>The Uri value or null if the property doesn't exist.</returns>
public static Uri? GetUriProperty(JsonElement root, string propertyName)
=> GetPropertyValue(root, propertyName, element => new Uri(element.GetString()!, UriKind.RelativeOrAbsolute));

/// <summary>
/// Safely extracts a value from a dictionary.
/// </summary>
/// <param name="attributes">The dictionary containing the value.</param>
/// <param name="key">The key of the value to extract.</param>
/// <returns>The value or null if the key doesn't exist.</returns>
public static string? GetAttributeValue(Dictionary<string, string> attributes, string key)
{
return attributes.TryGetValue(key, out var value) ? value : null;
}
}
45 changes: 45 additions & 0 deletions src/AWS.Messaging/Serialization/MessageMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace AWS.Messaging.Serialization;

/// <summary>
/// Represents metadata associated with a message, including SQS, SNS, and EventBridge specific information.
/// </summary>
internal class MessageMetadata
{
/// <summary>
/// Gets or sets the SQS-specific metadata.
/// </summary>
public SQSMetadata? SQSMetadata { get; set; }

/// <summary>
/// Gets or sets the SNS-specific metadata.
/// </summary>
public SNSMetadata? SNSMetadata { get; set; }

/// <summary>
/// Gets or sets the EventBridge-specific metadata.
/// </summary>
public EventBridgeMetadata? EventBridgeMetadata { get; set; }

/// <summary>
/// Initializes a new instance of the MessageMetadata class.
/// </summary>
public MessageMetadata()
{
}

/// <summary>
/// Initializes a new instance of the MessageMetadata class with specified metadata.
/// </summary>
/// <param name="sqsMetadata">The SQS metadata.</param>
/// <param name="snsMetadata">The SNS metadata.</param>
/// <param name="eventBridgeMetadata">The EventBridge metadata.</param>
public MessageMetadata(
SQSMetadata? sqsMetadata = null,
SNSMetadata? snsMetadata = null,
EventBridgeMetadata? eventBridgeMetadata = null)
{
SQSMetadata = sqsMetadata;
SNSMetadata = snsMetadata;
EventBridgeMetadata = eventBridgeMetadata;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using Amazon.SQS.Model;
using AWS.Messaging.Serialization.Handlers;

namespace AWS.Messaging.Serialization.Parsers
{
/// <summary>
/// Parser for messages originating from Amazon EventBridge.
/// </summary>
internal class EventBridgeMessageParser : IMessageParser
{
/// <summary>
/// Determines if the JSON element represents an EventBridge message by checking for required properties.
/// </summary>
/// <param name="root">The root JSON element to examine.</param>
/// <returns>True if the message can be parsed as an EventBridge message; otherwise, false.</returns>
public bool CanParse(JsonElement root)
{
return root.TryGetProperty("detail", out _) &&
root.TryGetProperty("detail-type", out _) &&
root.TryGetProperty("source", out _) &&
root.TryGetProperty("time", out _);
}

/// <summary>
/// Parses an EventBridge message, extracting the message body and metadata.
/// </summary>
/// <param name="root">The root JSON element containing the EventBridge message.</param>
/// <param name="originalMessage">The original SQS message.</param>
/// <returns>A tuple containing the message body and associated metadata.</returns>
/// <exception cref="InvalidOperationException">Thrown when the EventBridge message does not contain a valid detail property.</exception>
public (string MessageBody, MessageMetadata Metadata) Parse(JsonElement root, Message originalMessage)
{
// The detail property can be either a string or an object
var detailElement = root.GetProperty("detail");

// Add explicit check for null detail
if (detailElement.ValueKind == JsonValueKind.Null)
{
throw new InvalidOperationException("EventBridge message does not contain a valid detail property");
}

var messageBody = detailElement.ValueKind == JsonValueKind.String
? detailElement.GetString()
: detailElement.GetRawText();

if (string.IsNullOrEmpty(messageBody))
{
throw new InvalidOperationException("EventBridge message does not contain a valid detail property");
}

var metadata = new MessageMetadata
{
EventBridgeMetadata = MessageMetadataHandler.CreateEventBridgeMetadata(root)
};

return (messageBody, metadata);
}
}
}
29 changes: 29 additions & 0 deletions src/AWS.Messaging/Serialization/Parsers/IMessageParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using Amazon.SQS.Model;
using AWS.Messaging.Serialization;

namespace AWS.Messaging.Serialization.Parsers;

/// <summary>
/// Defines the contract for message parsers capable of handling different message formats.
/// </summary>
internal interface IMessageParser
{
/// <summary>
/// Determines if the parser can handle the given JSON element.
/// </summary>
/// <param name="root">The root JSON element to examine.</param>
/// <returns>True if the parser can handle the message; otherwise, false.</returns>
bool CanParse(JsonElement root);

/// <summary>
/// Parses the message, extracting the message body and associated metadata.
/// </summary>
/// <param name="root">The root JSON element containing the message to parse.</param>
/// <param name="originalMessage">The original SQS message.</param>
/// <returns>A tuple containing the extracted message body and associated metadata.</returns>
(string MessageBody, MessageMetadata Metadata) Parse(JsonElement root, Message originalMessage);
}
49 changes: 49 additions & 0 deletions src/AWS.Messaging/Serialization/Parsers/SNSMessageParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using Amazon.SQS.Model;
using AWS.Messaging.Serialization.Handlers;

namespace AWS.Messaging.Serialization.Parsers
{
/// <summary>
/// Parser for messages originating from Amazon Simple Notification Service (SNS).
/// </summary>
internal class SNSMessageParser : IMessageParser
{
/// <summary>
/// Determines if the JSON element represents an SNS message by checking for required properties.
/// </summary>
/// <param name="root">The root JSON element to examine.</param>
/// <returns>True if the message can be parsed as an SNS message; otherwise, false.</returns>
public bool CanParse(JsonElement root)
{
return root.TryGetProperty("Type", out var type) &&
type.GetString() == "Notification" &&
root.TryGetProperty("MessageId", out _) &&
root.TryGetProperty("TopicArn", out _);
}

/// <summary>
/// Parses an SNS message, extracting the inner message body and metadata.
/// </summary>
/// <param name="root">The root JSON element containing the SNS message.</param>
/// <param name="originalMessage">The original SQS message.</param>
/// <returns>A tuple containing the extracted message body and associated metadata.</returns>
/// <exception cref="InvalidOperationException">Thrown when the SNS message does not contain a valid Message property.</exception>
public (string MessageBody, MessageMetadata Metadata) Parse(JsonElement root, Message originalMessage)
{
// Extract the inner message from the SNS wrapper
var messageBody = root.GetProperty("Message").GetString()
?? throw new InvalidOperationException("SNS message does not contain a valid Message property");

var metadata = new MessageMetadata
{
SNSMetadata = MessageMetadataHandler.CreateSNSMetadata(root)
};

return (messageBody, metadata);
}
}
}
Loading
Loading