# Message Writer Substitute

A substitute is an alternate implementation of an interface. In this case, the message writer interface.

For more information about substitutes in general, see the useful objects user guide.

The Messaging::Write class includes a substitute that allows for the inspection of messages written after the writer has been invoked. In this case the message writer substitute is a diagnostic substitute.

The writer substitute records the writes that have been actuated, and does not write messages to the message store database. Said otherwise, the substitute has no durable I/O side effects. As such, it's an inert substitute.

Note: The meaning of "substitute" in this context refers to Liskov Substitution Principle, which describes the quality of polymorphism in object-oriented systems as objects that can legitimately replace, or substitute, each other without changing the correctness or intention of the program that uses the substitute. From the standpoint of their shared interface, substitutes are not considered either more "real" or less real than other substitutes of the same interface. In this, all substitutes are real, and no substitute is considered secondary.

# Example

class SomeHandler
  dependency :write, Messaging::Postgres::Write

  # ...

  handle SomeMessage do |some_message|
    other_message = OtherMessage.follow(some_message)

    write.(other_message, stream_name)
  end
end

handler = SomeHandler.new

handler.write.class
# =>  Messaging::Write::Substitute

# The result of handling some_message is that
# another message will be written
handler.(some_message)

# Since the handler writes a message, the writer
# substitute will respond in the affirmative
# to a query of whether the message was written
handler.write.written? { |message| message.instance_of? SomeMessage }
# => true

# Writer Substitute Facts

  • The writer substitute does not write messages to the message store database
  • Each write is recored as telemetry that is kept in memory for the lifetime of the writer object
  • The substitute offers convenience methods for inspecting the telemetry recorded for each write
  • Telemetry is recorded for all invocations of the writer, including the initial and reply methods
  • By default, a writer declared with the dependency macro will be initialized to the writer's substitute.

# Messaging::Write::Substitute::Write Class

The Substitute::Write class is a concrete class from the Messaging library in the Messaging::Write namespace.

The Messaging::Write::Substitute::Write class provides:

  • Inert implementations of the writer's instance actuator method, as well as the initial and the reply methods.
  • A telemetry recorder with an active telemetry sink that records telemetry recorded by invocations of the writer's methods
  • A telemetry data structure for recording each individual invocation of the writer

# Determine If a Message Was Written

written?(message=nil, blk)

Returns

true if the arguments match any messages written and recorded as telemetry.

Parameters

Name Description Type
message A message that may have been written Messaging::Message
blk A block into which each telemetry record is passed, and returns true for the first record for which block is not false Proc

Block Parameters

The following parameters are passed to the blk if blk is passed and thus evaluated:

Name Description Type
message A message that may have been written Messaging::Message
stream_name The stream name passed to the invocation of the writer String
expected_version Expected version passed to the invocation of the writer Integer
reply_stream_name Name of the reply stream passed to the invocation of the writer String

Permutations

If both the message and blk arguments are omitted, then true is returned if there is any record of a write.

If the message argument is passed and the blk argument is omitted, then true is returned if any recorded message equals the value of the message argument. Equality is tested using the == equality operator.

If the message argument is omitted, the block is evaluated for each telemetry data recorded, and true is returned if the block evaluates to true for any recorded telemetry data.

If both the message argument and the blk argument are passed, then both have to evaluate to true for true to be returned. In this case, the message is not passed to the block. Only the stream_name, expected_version, and reply_stream_name are passed to the block, and the message equality is tested using the == equality operator.

Examples

message = SomeMessage.new
stream_name = 'someStream-123'

write.(message, stream_name, expected_version: -1, reply_stream_name: 'someReplyStream')

# Returns true if there is a record of any write
write.written?
# => true

# Returns true if any recorded message equals the value of
# the message argument
# Equality is tested using ==
write.written?(message)
# => true

# Returns false since the message written does not
# equal Object.new
some_object = Object.new
write.written?(some_object)
# => false

# Block form passes the message written as an
# argument to the block, making the message
# visible to the block, and available for inspection
write.written? { |message| message == some_message }
# => true

# Block form with message and stream_name visibility
write.written? do |message, stream_name|
  message.instance_of? SomeMessage &&
    stream_name == 'someStream-123'
end
# => true

# Block form with message, stream_name, expected_version,
# and reply_stream_name visibility
write.written? do |message, stream_name, expected_version, reply_stream_name|
  message.instance_of? SomeMessage &&
    stream_name == 'someStream-123' &&
    expected_version == -1 &&
    reply_stream_name == 'someReplyStream'
end
# => true

# Note: First block argument is stream_name when message
# argument is passed
write.written?(message) do |stream_name, expected_version, reply_stream_name|
  stream_name == 'someStream-123' &&
    expected_version == -1 &&
    reply_stream_name == 'someReplyStream'
end
# => true

# Query the Messages Written

message_writes(blk)

Returns

Array containing the messages written or messages for which the given block returns true.

Alias

writes

Parameters

Name Description Type
blk A block into which each telemetry record is passed and evaluated for inclusion in the result Proc

Block Parameters

The following parameters are passed to the blk if blk is passed and thus evaluated:

Name Description Type
message A message that may have been written Messaging::Message
stream_name The stream name passed to the invocation of the writer String
expected_version Expected version passed to the invocation of the writer Integer
reply_stream_name Name of the reply stream passed to the invocation of the writer String

Permutations

If the blk argument is omitted, all of the messages recorded are returned.

If blk argument is passed, the block is evaluated for each telemetry data recorded, and the messages that match are returned.

Examples

message = SomeMessage.new
other_message = OtherMessage.new

stream_name = 'someStream-123'

write.(message, stream_name)
write.(other_message, stream_name)

# Without the block, returns all
write.writes
# => [ <SomeMessage...>, <OtherMessage...> ]

write.writes { |message| message.instance_of? SomeMessage }
# => [ <SomeMessage...> ]

# Query One Message Written

one_message_write(blk)

Returns one matching message, or raises Messaging::Write::Substitute::Write::Error if there are more than one matching message.

Returns

The message written or the message for which the given block returns true.

Alias

one_message

Note: Works exactly as does the message_writes method with the exception of raising an error if more than one record is found.

Example

message = SomeMessage.new

stream_name = 'someStream-123'

write.(message, stream_name)

write.one_message
# => <SomeMessage...>

write.one_message { |message| message.instance_of? SomeMessage }
# => <SomeMessage...>


some_message_2 = SomeMessage.new
write.(some_message_2, stream_name)

write.one_message
# => Messaging::Write::Substitute::Write::Error

write.one_message { |message| message.instance_of? SomeMessage }
# => Messaging::Write::Substitute::Write::Error

# Query the Telemetry Records of Writes

writes(blk)

Returns

Array containing the telemetry records for which the given block returns true.

Note: Same functionality and interface as the message_writes method, except returns telemetry records instead of messages.

# Telemetry

The writer records telemetry for each invocation of the writer.

Note: Telemetry is implemented using the Telemetry library. For background on how the Telemetry library works and how to use it, see: https://github.com/eventide-project/telemetry.

Two different kinds of telemetry signals are recorded, depending on which invocation of the writer was used:

  • :written
  • :replied

The raw telemetry records can be accessed directly through the substitute writer's interface:

# Records of the invocation of the writer's actuator
writer.sink.written_records

# Records of the invocation of the writer's `reply` method
writer.sink.replied_records

For each write recorded, an instance of the Messaging::Write::Telemetry::Data struct is created and assigned to the telemetry record's data attribute.

The Messaging::Write::Telemetry::Data struct has the following attributes:

  • message
  • stream_name
  • expected_version
  • reply_stream_name

The recorded data can be accessed directly:

writer.sink.written_records[i].data.message
writer.sink.written_records[i].data.stream_name
writer.sink.written_records[i].data.expected_version
writer.sink.written_records[i].data.reply_stream_name

# Causing the Expected Version Error to be Raised

raise_expected_version_error!()

Causes MessageStore::ExpectedVersion::Error to be raised upon the next invocation of the writer.

# Determine If a Message Is a Reply

replied?(message=nil, blk)

Note: Same functionality and interface as the written?method, except for replies.

# Query the Messages Replies

message_replies(blk)

Note: Same functionality and interface as the message_writes method, except for replies.

# Query One Reply Message

one_message_reply(blk)

Alias

one_reply

Note: Same functionality and interface as the one_message_write method, except for replies.

# Query the Telemetry Records of Replies

replies(blk)

Note: Same functionality and interface as the writes method, except for replies.

# Constructing a Writer Substitute

The writer substitute can be constructed in one of two ways:

  • Via the constructor
  • Via the dependency macro

# Via the Constructor

Messaging::Write::Substitute.build()

Returns

Instance of the Messaging::Write::Substitute::Write class.

# Via the dependency Macro

dependency :write, Messaging::Postgres::Write

A writer declared with the dependency macro will be initialized to the writer's substitute.

class SomeHandler
  dependency :write, Messaging::Postgres::Write

  # ...
end

handler = SomeHandler.new

handler.write.class
# =>  Messaging::Write::Substitute::Write

TIP

See the useful objects user guide for background on using dependencies and their substitutes.