# Messages
Messages are packages of data that are the principle means of transmission of instructions and status between services. They carry data between services and processes. Messages typically represent commands or events, and are recorded or sent via the message store (or other message transport).
# Example
# A command
class Withdraw
include Messaging::Message
attribute :withdrawal_id, String
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
end
# An event
class Withdrawn
include Messaging::Message
attribute :withdrawal_id, String
attribute :account_id, String
attribute :amount, Numeric
attribute :time, String
attribute :processed_time, String
end
# Message Facts
- A message class is a data object that has data attributes with optional data type checking
- Events and commands are kinds of messages
- A message contains optional metadata that is separate from the message data
- Messages are transformed to and from raw MessageStore::MessageData when storing and retrieving from the message store
- Message data is formatted as JSON when stored
- Messages are typically flat key/value structures, and by default, transformation of messages does not traverse a graph of attributes
# Messaging::Message Module
A class becomes a message by including the Message
module from the Messaging
library and namespace.
The Messaging::Message
module affords the receiver with:
- The
attribute
macro for declaring message attributes - The
build
constructor that optionally receives a hash or attribute data and a hash of metadata attribute data - The
attributes
method (aliased asto_h
) that returns a hash of attribute name and attribute value pairs - The
attribute_names
method that returns an array of attribute names - The
copy
method that copies a message's data to another message - The
follow
constructor that takes a message and constructs another message based on the former message's data and metadata - The
follows?
predicate that determines whether a message was constructed from a former message - A
==
operator implementation that determines message equality based on attribute values and message class - The
message_type
class method that returns a string representation of the message class name - The
message_type?
class predicate that determines when a message's message type matches the argument - The
message_name
class method that returns the message's type in underscore case
# Constructing a Message
# Via the Initializer
A message's initializer does not receive any data passed to it.
withdraw = Withdraw.new
# Via the Constructor
A message has a constructor named build
that allows for optionally providing a hash of data and a hash of metadata at the time of construction.
self.build(data={}, metadata={})
Returns
An instance of the message class
Parameters
Name | Description | Type |
---|---|---|
data | Hash of data whose key names match the message's attribute names | Hash |
metadata | Hash of data whose key names match message metadata attribute names | Hash |
data = {
withdrawal_id: 'ABC',
account_id: '123',
amount: 11,
time: '2000-01-01T00:00:00.000Z'
}
withdraw = Withdraw.build(data)
# => #<Withdraw:0x... @withdrawal_id="ABC", @account_id="123", @amount=11, @time="2000-01-01T00:00:00.000Z">
# Construct a Hash Representation of the Message's Data
attributes()
Alias
to_h
Returns
Hash of attribute names and values.
withdraw = Withdraw.build(data)
data = withdraw.attributes # or withdraw.to_h
# => {:withdrawal_id=>"ABC", :account_id=>"123", :amount=>11, :time=>"2000-01-01T00:00:00.000Z"}
# Attribute Names
A list of the message's attribute names can be retrieved from its class interface.
self.attribute_names
Returns
Array of attribute names.
Withdraw.attribute_names
# => [:withdrawal_id, :account_id, :amount, :time]
# Reserved Attributes
A message object has two reserved attributes.
Name | Description | Type |
---|---|---|
id | Unique identifier of the message | UUID String |
metadata | Mechanical data describing the message and its provenance | Messaging::Message::Metadata |
DANGER
Reserved attributes are for system use only and cannot be overwritten without causing a message to become incompatible with the rest of the toolkit, and without causing malfunctions or failures.
# Metadata
A message's metadata object contains information about the stream where the message resides, the previous message in a series of messages that make up a messaging workflow, the originating process to which the message belongs, as well as other data that are pertinent to understanding the provenance and disposition of the message.
See Metadata for more.
# Copying Message Attribute Data to Another Message
The copy
class method constructs a message from another message's data.
self.copy(source, copy: [], include: [], exclude: [], strict: false, metadata: false)
Returns
Instance of the receiver message class initialized with the source message's data.
Parameters
Name | Description | Type |
---|---|---|
source | Message to build the subsequent message from | Message |
copy | Whitelist of attribute names to copy | Array of Symbols |
include | Alias for the copy parameter | Array of Symbols |
exclude | Blacklist of attribute names to exclude from copying | Array of Symbols |
strict | Raise an error if receiver doesn't define a setter for a whitelisted attribute when the value is true | Boolean |
metadata | Copies the message metadata as well when the value is true | Boolean |
WARNING
Setting the value of metadata
to true
should be used with extreme caution, and has no practical use in everyday applicative logic. Except for certain testing and infrastructural scenarios, copying the identifying metadata from one message to another can result in significant malfunctions if the copied message is then written to a stream and processed.
class SourceMessage
include Messaging::Message
attribute :some_attribute
attribute :some_other_attribute
attribute :yet_another_attribute
end
class ReceiverMessage
include Messaging::Message
attribute :some_attribute
attribute :some_other_attribute
attribute :yet_another_attribute
end
source_message = SourceMessage.new()
source_message.some_attribute = 'some value'
source_message.some_other_attribute = 'some other value'
source_message.yet_another_attribute = 'yet another value'
receiver_message = ReceiverMessage.copy(source_message)
# => #<ReceiverMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
receiver_message = ReceiverMessage.copy(source_message, copy: [:some_attribute, :some_other_attribute])
# => #<ReceiverMessage:0x... @some_attribute="some value", @some_other_attribute="some other value">
receiver_message = ReceiverMessage.copy(source_message, exclude: [:some_attribute, :some_other_attribute])
# => #<ReceiverMessage:0x... @yet_another_attribute="yet another value">
# Strictness
An error is raised when the receiver doesn't define a setter for a whitelisted attribute and the value of the strict
parameter is true
Strictness for the copy
method defaults to false
.
class SourceMessage
attribute :additional_attribute
end
source_message.additional_attribute = 'additional value'
receiver_message = ReceiverMessage.copy(source_message, strict: true)
# => Messaging::Message::Copy::Error (#<ReceiverMessage:0x...> has no setter for additional_attribute)
receiver_message = ReceiverMessage.copy(source_message)
# => #<ReceiverMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
# Attribute Map
The whitelist of attributes specified with the copy
parameter can include maps of attributes, allowing the data to be copied between attributes with different names.
receiver_message = ReceiverMessage.copy(source_message, copy: [
{ :some_attribute => :some_other_attribute },
:yet_another_attribute
])
# => #<ReceiverMessage:0x... @some_other_attribute="some value", @yet_another_attribute="yet another value">
# Messaging::Message::Copy Module
The copy
class method of a message class can be also be actuated from the Messaging::Message::Copy
module.
The underlying implementation of a message class's copy
method is the Messaging::Message::Copy
module. It can be actuated either by including it, or via invoking its methods directly as module methods.
self.call(source, receiver=nil, copy: [], include: [], exclude: [], strict: false, metadata: false)
Returns
Instance of the receiver message class initialized with the source message's data, or the receiver message object after having the source's data copied to it.
Parameters
Name | Description | Type |
---|---|---|
source | Message to build the subsequent message from | Message |
receiver | The message that will receive the source message's data | Message class or message instance |
copy | Whitelist of attribute names to copy | Array of Symbols |
include | Alias for the copy parameter | Array of Symbols |
exclude | Blacklist of attribute names to exclude from copying | Array of Symbols |
strict | Raise an error if receiver doesn't define a setter for a whitelisted attribute when the value is true | Boolean |
metadata | Copies the message metadata as well when the value is true | Boolean |
Receiver is an Instance of a Message Class
When the receiver is a message instance, the class will be constructed, and then the source message's data will be copied to it.
receiver_message = ReceiverMessage.new
Messaging::Message::Copy.(source_message, receiver_message)
# => #<ReceiverMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
Receiver is a Message Class
When the receiver is a message class, the class will be constructed, and then the source message's data will be copied to it.
receiver_message = Messaging::Message::Copy.(source_message, ReceiverMessage)
# => #<ReceiverMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
Note that when the Messaging::Message::Copy
is extended onto a message class, the default value of the receiver
parameter is self
. The value of self
in such a case is the message class.
# Message Workflows
Messages frequently represent subsequent steps or stages in a process. Subsequent messages follow after preceding messages. Data from the preceding message is copied to the subsequent messages, and parts of the subsequent message's metadata are constructed with the data from the preceding message.
Constructing a message from a preceding message in a message flow is a common pattern in handler implementation.
self.follow(preceding_message, copy: [], include: [], exclude: [], strict: true)
Returns
Instance of the subsequent message class initialized with the preceding message's data.
Parameters
Name | Description | Type |
---|---|---|
preceding_message | Message to build the subsequent message from | Message |
copy | Whitelist of attribute names to copy | Array of Symbols |
include | Alias for the copy parameter | Array of Symbols |
exclude | Blacklist of attribute names to exclude from copying | Array of Symbols |
strict | Raise an error if receiver doesn't define a whitelisted attribute | Boolean |
Following a message has almost identical behavior to a message class's copy method. The follow
message leverages the implementation of copy
to fulfill its purpose.
class PrecedingMessage
include Messaging::Message
attribute :some_attribute
attribute :some_other_attribute
attribute :yet_another_attribute
end
class SubsequentMessage
include Messaging::Message
attribute :some_attribute
attribute :some_other_attribute
attribute :yet_another_attribute
end
preceding_message = PrecedingMessage.new()
preceding_message.some_attribute = 'some value'
preceding_message.some_other_attribute = 'some other value'
preceding_message.yet_another_attribute = 'yet another value'
subsequent_message = SubsequentMessage.follow(preceding_message)
# => #<SubsequentMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
subsequent_message = SubsequentMessage.follow(preceding_message, copy: [:some_attribute, :some_other_attribute])
# => #<SubsequentMessage:0x... @some_attribute="some value", @some_other_attribute="some other value">
subsequent_message = SubsequentMessage.follow(preceding_message, exclude: [:some_attribute, :some_other_attribute])
# => #<SubsequentMessage:0x... @yet_another_attribute="yet another value">
# Strictness
An error is raised when the receiver doesn't define a setter for a whitelisted attribute and the value of the strict
parameter is true
Strictness for the follow
method defaults to true
.
This helps avoid unintended errors that can happen when permissive copying happens between two message schemas. The strictness of the follow
method requires that the use of the whitelist and blacklist of attribute names to be copied or omitted explicitly expresses the expectations of the copying of message attributes in a messaging workflow.
class PrecedingMessage
attribute :additional_attribute
end
preceding_message.additional_attribute = 'additional value'
subsequent_message = SubsequentMessage.follow(preceding_message)
# => Messaging::Message::Copy::Error (#<SubsequentMessage:0x...> has no setter for additional_attribute)
subsequent_message = SubsequentMessage.follow(preceding_message, strict: false)
# => #<SubsequentMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
# Attribute Map
The whitelist of attributes specified with the copy
parameter can include maps of attributes, allowing the data to be copied between attributes with different names.
subsequent_message = SubsequentMessage.follow(preceding_message, copy: [
{ :some_attribute => :some_other_attribute },
:yet_another_attribute
])
# => #<SubsequentMessage:0x... @some_attribute="some value", @yet_another_attribute="yet another value">
# Metadata and Message Workflows
While copying message data from a preceding message to a subsequent message is a convenient feature, copying message flow properties between the metadata of two messages is the feature that implements message workflow.
Refer to Metadata for a more complete description of metadata and message flows.
# Messaging::Message::Follow Module
The follow
class method of a message class can be also be actuated from the Messaging::Message::Follow
module.
The underlying implementation of a message class's follow
method is the Messaging::Message::Follow
module. It can be actuated either by including it, or via invoking its methods directly as module methods.
self.call(preceding_message, subsequent_message=nil, copy: nil, include: nil, exclude: nil, strict: nil)
Returns
Instance of the subsequent message class initialized with the preceding message's data, or the subsequent message object after having the preceding message's data copied to it.
Parameters
Name | Description | Type |
---|---|---|
preceding_message | Message to build the subsequent message from | Message |
subsequent_message | The message that will receive the preceding message's data | Message class or message instance |
copy | Whitelist of attribute names to copy | Array of Symbols |
include | Alias for the copy parameter | Array of Symbols |
exclude | Blacklist of attribute names to exclude from copying | Array of Symbols |
strict | Raise an error if receiver doesn't define a whitelisted attribute | Boolean |
Subsequent Message is an Instance of a Message Class
When the receiver is a message instance, the class will be constructed, and then the source message's data and message flow metadata will be copied to it.
subsequent_message = SubsequentMessage.new()
Messaging::Message::Follow.(preceding_message, subsequent_message)
# => #<SubsequentMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
Subsequent Message is a Message Class
When the receiver is a message class, the class will be constructed, and then the source message's data and message flow metadata will be copied to it.
subsequent_message = Messaging::Message::Follow.(preceding_message, SubsequentMessage)
# => #<SubsequentMessage:0x... @some_attribute="some value", @some_other_attribute="some other value", @yet_another_attribute="yet another value">
Note that when the Messaging::Message::Follow
is extended onto a message class, the default value of the subsequent_message
parameter is self
. The value of self
in such a case is the message class.
# Determining Message Precedence
Messages can be determined to follow each other using the message's follows?
predicate method.
follows?(message)
Returns
Boolean.
Parameters
Name | Description | Type |
---|---|---|
message | A message instance that may precede the message being inspected | Message |
The follows?
predicate method returns true
when the message metadata's causation and provenance attributes match the message argument's metadata source attributes.
preceding_message = SomeMessage.new()
preceding_message.metadata.stream_name = 'someStream'
preceding_message.metadata.position = 11
preceding_message.metadata.global_position = 111
preceding_message.metadata.correlation_stream_name = 'someReplyStream'
preceding_message.metadata.reply_stream_name = 'someReplyStream'
message = SomeMessage.follow(preceding_message)
message.follows?(preceding_message)
# => true
preceding_message.metadata.stream_name = `someOtherStream`
message.follows?(preceding_message)
# => false
Message precedence is determined as:
message.metadata.causation_message_stream_name == preceding_message.metadata.stream_name &&
message.metadata.causation_message_position == preceding_message.metadata.position &&
message.metadata.causation_message_global_position == preceding_message.metadata.global_position &&
message.metadata.correlation_stream_name == preceding_message.metadata.correlation_stream_name &&
message.metadata.reply_stream_name == preceding_message.metadata.reply_stream_name
However, the correlation_stream_name
attribute and the reply_stream_name
attribute is only a factor in determining precedence if their values are assigned on the preceding message metadata. If the preceding message metadata's correlation_stream_name
attribute or reply_stream_name
attribute is nil
, then it is not taken into consideration for message precedence. Therefore, the preceding message metadata's correlation_stream_name
attribute or reply_stream_name
attribute can be nil
while the following message's correlation_stream_name
attribute or reply_stream_name
attribute are set to a value, and the messages are considered precedent.
In addition, if both the preceding message metadata's stream_name
attribute and the following message metadata's causation_stream_name
attribute are both nil
the messages are not considered precedent. The same is true for the position
and causation_message_position
pair and the global_position
and causation_message_global_position
pair.
The implementation of the metadata's follows?
predicate method is the best resource for understanding the specifics of message precedence. The source code can be read at:
# Equality
Two messages are considered to be equal when their classes are the same and their attribute values are the same.
class SomeMessage
include Messaging::Message
attribute :some_attribute
end
some_message_1 = SomeMessage.new
some_message_1.some_attribute = 'some value'
some_message_2 = SomeMessage.new
some_message_2.some_attribute = 'some value'
some_message_1 == some_message_2
# => true
some_message_2.some_attribute = 'some other value'
some_message_1 == some_message_2
# => false
class SomeOtherMessage
include Messaging::Message
attribute :some_attribute
end
some_other_message = SomeOtherMessage.new
some_other_message.some_attribute = 'some value'
some_message_1 == some_other_message
# => false
# Transformation To and From MessageData
MessageStore::MessageData is the raw, low-level storage representation of a message.
Before a message can be written to the message store, it's transformed into a MessageData
. When a message is retrieved from the message store, it's retrieved as a MessageData
object and then transformed to a message.
There are two implementations of MessageData
: MessageStore::MessageData::Write
that is the form that can be written to the message store, and MessageStore::MessageData::Read
that is the form that is retrieved from the message store.
# Export a Message to MessageData::Write
Messaging::Message::Export.call(message)
Returns
An instance of MessageStore::MessageData::Write
.
Parameters
Name | Description | Type |
---|---|---|
message | The message to export to MessageData | Message |
class SomeMessage
include Messaging::Message
attribute :some_attribute
end
some_message = SomeMessage.new
some_message.some_attribute = 'some value'
Messaging::Message::Export.(some_message)
# => <MessageStore::MessageData::Write:0x... @type="SomeMessage", @data={:some_attribute=>"some value"}>
# Import a Message from MessageData::Read
Messaging::Message::Import.call(message_data, message_class)
Returns
An instance of the message class.
Parameters
Name | Description | Type |
---|---|---|
message_data | The raw MessageData representation of a message | MessageData |
message_class | The message class to transform the MessageData into | Class |
message_data = MessageStore::MessageData::Read.new
message_data.data = { some_attribute: 'some value' }
message_data.type = 'SomeMessage'
some_message = Messaging::Message::Import.(message_data, SomeMessage)
# => #<SomeMessage:0x... @some_attribute="some value">
# Transforming Messages with Nested Objects
A message with a flat structure of attributes with strings, numbers, and time values can be converted to-and-from the underlying MessageData
format without any additional coding.
When a message has an attribute that contains an object rather than a primitive value, the message must implement two methods that transform the nested object to and from hash data: transform_read
and transform_write
. Hash data is the de facto intermediate representation of message data when transforming between messages and message data.
class SomeNestedObject
include Schema::DataStructure
attribute :something, String
attribute :something_else, String
end
class SomeMessage
include Schema::DataStructure
attribute :name, String
attribute :nested_objects, Collection::Set[SomeNestedObject],
default: -> { Collection::Set[SomeNestedObject].new }
def transform_read(data)
nested_objects = data[:nested_objects] &.map do |nested_object_data|
SomeNestedObject.build(nested_object_data)
end
nested_objects_set = Collection::Set[SomeNestedObject].new.add(nested_objects)
data[:nested_objects] = nested_objects_set
end
def transform_write(data)
nested_objects = data[:nested_objects]
if nested_objects.empty?
data.delete(:nested_objects)
else
data[:nested_objects] = nested_objects.map(&:to_h)
end
end
end
# Message Type
The message type is the name of the message class without any of the namespace information.
self.message_type()
Returns
String
SomeNamespace::SomeMessage.message_type
# => "SomeMessage"
# Message Type Predicate
The message type predicate method determines whether a string matches the message type of a message class.
self.message_type?(message_type)
Returns
String
Parameters
Name | Description | Type |
---|---|---|
message_type | Message type to compare with the message's message type | String |
SomeNamespace::SomeMessage.message_type?("SomeMessage")
# => true
SomeNamespace::SomeMessage.message_type?("SomeOtherMessage")
# => false
# Message Name
A message's name is the underscore cased formatted name of the message class without any of the namespace information.
self.message_name()
Returns
String.
SomeNamespace::SomeMessage.message_name
# => "some_message"