The typed testing builders describe a message as an ordered pipeline. The first operation initializes a message and each following operation appends an entry or customizes a produced value. Applying % to an unmaterialized entry macro customizes the entry created by that stage. A pipeline can populate a named holder immediately or produce a reusable recipe.
Pass a mutable holder as the first argument to initialize to populate it immediately. operator| returns the same holder, so entry operations can be chained from left to right.
Calling initialize again resets the message, including its repeating groups, before assigning the new fields. Direct pipelines are useful when a test needs to inspect or further modify one message before sending it.
For most message families, the direct form is Test::initialize(holder, parameters...) and the recipe form is Test::initialize<Test::Holder>(parameters...). See the testing namespace API reference for the complete overload signatures, defaults, and exception contracts.
Test::InstrumentDefinitionFuture additionally requires a Messaging::SecurityUpdateAction template argument. Its recipe form is Test::initialize<Test::InstrumentDefinitionFuture, Action>(parameters...); its direct form is Test::initialize<Action>(holder, parameters...). Supply lastUpdateTime explicitly when a test requires deterministic timestamps.
The entry macros are deferred pipeline stages:
The unqualified action passed to a macro is checked against the message type at compile time. For example, an MBP incremental accepts New, Change, Delete, DeleteThru, DeleteFrom, and Overlay where appropriate, while a snapshot accepts only New entries.
Order-ID entries use the OrderUpdateAction vocabulary: New, Update, and Delete.
Use Test::addEntries<Action>(message, range, projection) when entry data is selected or generated at runtime. The projection is called once for each input value and returns a tuple-like object containing the arguments for the message's existing typed entry operation. Inputs are consumed once in range order, so single-pass ranges and C++20 views are supported without requiring contiguous storage.
The action is checked against the destination message at compile time just as it is for ONIXS_CMEMDH_ENTRY. The range overload is available for the primary entry group of each message listed under "Supported typed builders"; auxiliary order-ID, trade-fill, and quote-entry groups continue to use their dedicated builders.
addEntries traverses the source once without first copying it. Keep the range and projected references valid while the destination grows, and do not structurally modify the destination from the projection. In particular, copy values out of a later repeating group before growing an earlier group that could relocate it. A failure does not roll back entries completed by earlier iterations.
Use Test::entries<Action>(range, projection) to append the primary entries from a runtime range to a reusable recipe. Unlike addEntries, this recipe stage consumes the complete range while the stage is appended. It invokes the projection once per input value and decay-copies every element of each projected tuple into recipe-owned storage. Repeated applications use those captured values without accessing the range or invoking the projection again. The following table-driven example builds reusable recipes from scenarios containing different entry sets.
The entries stage is recipe-only and single-use. Append it in the same expression so its borrowed range and projection remain valid. Appending the stage traverses the range and decay-copies each projected argument into recipe-owned storage. Explicit borrowing types such as pointers, views, and std::reference_wrapper remain borrows and must stay valid for every application. The stage can allocate while it captures the range and does not support % customization; shape individual entries in the projection instead.
Omit the holder argument and provide the target holder type as the initialize template argument to create a MessageRecipe. A recipe owns its operations and their decay-copied argument objects rather than a message buffer. It does not own storage referenced indirectly by pointer-like or view-like arguments. It can be applied repeatedly to holders or passed directly to MulticastFeed while any borrowed storage remains valid.
Calling a recipe with a mutable holder applies the recipe and returns that same holder by reference. applyTo remains available when no return value is needed. Pass recipes to standard algorithms through std::ref; this avoids copying the recipe and also supports recipes that own move-only customizations.
Appending an operation creates a new recipe and leaves a named base recipe unchanged. This allows tests to share a common setup and derive case-specific variants.
Operations run in their stored order and the recipe remains unchanged after either callable application or applyTo. The pipeline performs no dynamic allocation during application; a bulk entries stage allocates when it is appended.
Use % in infix form after initialization to customize the underlying message, or on an entry macro to customize the entry created by that stage. The holder, recipe, or deferred stage on the left provides the customization operator; namespace-qualified free-function calls such as Test::operator%(...) are not part of the API. % has higher precedence than |, so an adjacent customization binds to the entry on its left. Multiple % operations on one receiver run from left to right.
The callable return value is ignored. Deferred % customizations in direct pipelines and recipes must be invocable through a const reference, so mutable lambdas are not supported there. Only an immediate holder-level customization, holder % callable, may use a mutable callable.
The holder form is available only on a named mutable holder. The deferred-stage form consumes the stage, so an entry macro can be customized directly while a named stage must be passed through std::move and used only once.
An entry passed to an adjacent % customization is a temporary view into the holder's encoded storage. Do not retain that view after the callable returns. Growing a repeating group can move the encoded message tail and invalidate views of entries in later groups; reacquire entry views after adding entries or making other structural message changes.
Deferred stages are materialized in pipeline order while the expression is constructed. A completed recipe owns decay-copied arguments, but pointers, views, std::reference_wrapper, and reference captures continue to borrow their referenced storage. Quote-entry symbols are an exception: ONIXS_CMEMDH_QUOTE_ENTRY copies the symbol while its stage is materialized, so the completed operation or recipe owns those bytes and does not observe later changes to the original string.
A macro stage is single-use and may capture expressions by reference, so append it immediately rather than retaining or returning it. Storage referenced by Messaging::StrRef or another view must remain valid at a stable address until every recipe application completes unless the builder explicitly documents that it copies the value. If an operation throws, the holder retains preceding and partial mutations; MulticastFeed does not transmit when recipe materialization fails. See the API reference for the complete ownership and exception contracts.
Include <OnixS/CME/MDH/Testing.h> for the complete testing API, or <OnixS/CME/MDH/testing/Messages.h> when only the typed builders are needed. Each builder supplies an initialize overload and, where applicable, direct add... functions and deferred pipeline stages.
| Testing holder | CME template | Pipeline entries and actions |
|---|---|---|
| IncrementalRefreshBook | IncrementalRefreshBook46 | ONIXS_CMEMDH_ENTRY: value and delete-style MBP updates; ONIXS_CMEMDH_ORDER_ID_ENTRY: order-ID updates |
| IncrementalRefreshOrderBook | IncrementalRefreshOrderBook47 | ONIXS_CMEMDH_ENTRY: New, Change, and Delete MBO updates |
| IncrementalRefreshTradeSummary | IncrementalRefreshTradeSummary48 | ONIXS_CMEMDH_ENTRY: New, Change, and Delete; ONIXS_CMEMDH_TRADE_ENTRY: order-level fills |
| IncrementalRefreshSessionStatistics | IncrementalRefreshSessionStatistics51 | ONIXS_CMEMDH_ENTRY: New and Delete statistics |
| SnapshotFullRefresh | SnapshotFullRefresh52 | ONIXS_CMEMDH_ENTRY: New MBP snapshot entries |
| SnapshotFullRefreshOrderBook | SnapshotFullRefreshOrderBook53 | ONIXS_CMEMDH_ENTRY: New MBO snapshot entries |
| InstrumentDefinitionFuture | InstrumentDefinitionFuture54 | Initialization for Add, Modify, or Delete |
| SecurityStatus | SecurityStatus30 | Initialization of a security and its trading status |
| QuoteRequest | QuoteRequest39 | ONIXS_CMEMDH_QUOTE_ENTRY: related symbols |
The following fragment shows the pipeline shape for the less common entry families.
Every typed holder is a specialization of MessageHolder. It owns fixed-capacity storage containing a CME MessagePrefix followed by the encoded message. The holder is neither copyable nor movable because its message view refers to that storage.
MessageHolder::setHeader commits the current encoded payload size to the prefix. toNetworkBytes commits the prefix and returns a non-owning ByteSpan; asSpan returns the currently committed bytes without refreshing the prefix. MulticastFeed, PacketHolder::toNetworkMessageSpans, and the free asSpan helper commit headers automatically. When using focused includes, PacketHolder is declared in <OnixS/CME/MDH/testing/PacketHolder.h> and the free asSpan helper in <OnixS/CME/MDH/testing/Utils.h>.
Use PacketHolder when a packet has a fixed compile-time message layout. Its template arguments determine the order of messages on the wire.
In C++17, the same holders can be named with a structured binding. Each binding is a reference to the actual stored MessageHolder, including any explicitly configured capacity, and remains valid for the lifetime of the packet.
PacketHolder also provides ADL-visible free get<I> overloads for generic code that imports std::get and then calls get<I>(packet) unqualified. This user-defined tuple protocol does not promise portable std::apply support.
MessageHolder and PacketHolder provide toStr helpers and stream insertion for diagnostic output. ByteSpan provides toStr helpers.