Skip to main content

fix_codec_rs/
group.rs

1use crate::field::Field;
2use crate::tag::{self, Tag};
3
4/// Describes one repeating group in the FIX specification.
5///
6/// - `count_tag`: the `NO_*` tag that precedes the group and carries the instance count.
7/// - `delimiter_tag`: the first tag of every instance; its reappearance signals a new instance.
8/// - `member_tags`: all tags that may appear inside an instance (includes the delimiter tag).
9pub struct GroupSpec {
10    pub count_tag: Tag,
11    pub delimiter_tag: Tag,
12    pub member_tags: &'static [Tag],
13}
14
15// ---------------------------------------------------------------------------
16// FIX 4.2 built-in group specs
17// Source: https://www.onixs.biz/fix-dictionary/4.2/
18// ---------------------------------------------------------------------------
19
20/// NO_ALLOCS (78) — AllocAccount is the delimiter tag.
21pub const ALLOCS: GroupSpec = GroupSpec {
22    count_tag: tag::NO_ALLOCS,
23    delimiter_tag: tag::ALLOC_ACCOUNT,
24    member_tags: &[tag::ALLOC_ACCOUNT, tag::ALLOC_SHARES, tag::PROCESS_CODE],
25};
26
27/// NO_ORDERS (73) — ClOrdID is the delimiter tag.
28pub const ORDERS: GroupSpec = GroupSpec {
29    count_tag: tag::NO_ORDERS,
30    delimiter_tag: tag::CL_ORD_ID,
31    member_tags: &[
32        tag::CL_ORD_ID,
33        tag::LIST_SEQ_NO,
34        tag::WAVE_NO,
35        tag::ACCOUNT,
36        tag::SETTLMNT_TYP,
37        tag::FUT_SETT_DATE,
38        tag::HANDL_INST,
39        tag::EXEC_INST,
40        tag::MIN_QTY,
41        tag::MAX_FLOOR,
42        tag::EX_DESTINATION,
43        tag::OPEN_CLOSE,
44        tag::COVERED_OR_UNCOVERED,
45        tag::CUSTOMER_OR_FIRM,
46        tag::MAX_SHOW,
47        tag::PRICE,
48        tag::STOP_PX,
49        tag::PEG_DIFFERENCE,
50        tag::DISCRETION_INST,
51        tag::DISCRETION_OFFSET,
52        tag::CURRENCY,
53        tag::COMPLIANCE_ID,
54        tag::SOLICITED_FLAG,
55        tag::IOI_ID,
56        tag::TIME_IN_FORCE,
57        tag::EXPIRE_TIME,
58        tag::COMMISSION,
59        tag::RULE80A,
60        tag::FOREX_REQ,
61        tag::SETTL_CURRENCY,
62        tag::ORDER_QTY,
63        tag::CASH_ORDER_QTY,
64        tag::ORD_TYPE,
65        tag::SIDE,
66        tag::LOCATE_REQD,
67        tag::TRANSACT_TIME,
68        tag::SYMBOL,
69        tag::SYMBOL_SFX,
70        tag::SECURITY_ID,
71        tag::ID_SOURCE,
72        tag::SECURITY_TYPE,
73        tag::MATURITY_MONTH_YEAR,
74        tag::MATURITY_DAY,
75        tag::PUT_OR_CALL,
76        tag::STRIKE_PRICE,
77        tag::OPT_ATTRIBUTE,
78        tag::CONTRACT_MULTIPLIER,
79        tag::COUPON_RATE,
80        tag::SECURITY_EXCHANGE,
81        tag::ISSUER,
82        tag::SECURITY_DESC,
83        tag::TEXT,
84    ],
85};
86
87/// NO_RPTS (82) — RptSeq is the delimiter tag.
88pub const RPTS: GroupSpec = GroupSpec {
89    count_tag: tag::NO_RPTS,
90    delimiter_tag: tag::RPT_SEQ,
91    member_tags: &[tag::RPT_SEQ],
92};
93
94/// NO_DLVY_INST (85) — DlvyInst is the delimiter tag.
95pub const DLVY_INST: GroupSpec = GroupSpec {
96    count_tag: tag::NO_DLVY_INST,
97    delimiter_tag: tag::DLVY_INST,
98    member_tags: &[tag::DLVY_INST],
99};
100
101/// NO_EXECS (124) — ExecID is the delimiter tag.
102pub const EXECS: GroupSpec = GroupSpec {
103    count_tag: tag::NO_EXECS,
104    delimiter_tag: tag::EXEC_ID,
105    member_tags: &[
106        tag::EXEC_ID,
107        tag::LAST_SHARES,
108        tag::LAST_PX,
109        tag::LAST_CAPACITY,
110    ],
111};
112
113/// NO_MISC_FEES (136) — MiscFeeAmt is the delimiter tag.
114pub const MISC_FEES: GroupSpec = GroupSpec {
115    count_tag: tag::NO_MISC_FEES,
116    delimiter_tag: tag::MISC_FEE_AMT,
117    member_tags: &[tag::MISC_FEE_AMT, tag::MISC_FEE_CURR, tag::MISC_FEE_TYPE],
118};
119
120/// NO_RELATED_SYM (146) — RelatdSym is the delimiter tag.
121pub const RELATED_SYM: GroupSpec = GroupSpec {
122    count_tag: tag::NO_RELATED_SYM,
123    delimiter_tag: tag::RELATD_SYM,
124    member_tags: &[
125        tag::RELATD_SYM,
126        tag::SYMBOL_SFX,
127        tag::SECURITY_ID,
128        tag::ID_SOURCE,
129        tag::SECURITY_TYPE,
130        tag::MATURITY_MONTH_YEAR,
131        tag::MATURITY_DAY,
132        tag::PUT_OR_CALL,
133        tag::STRIKE_PRICE,
134        tag::OPT_ATTRIBUTE,
135        tag::CONTRACT_MULTIPLIER,
136        tag::COUPON_RATE,
137        tag::SECURITY_EXCHANGE,
138        tag::ISSUER,
139        tag::SECURITY_DESC,
140    ],
141};
142
143/// NO_IOI_QUALIFIERS (199) — IOIQualifier is the delimiter tag.
144pub const IOI_QUALIFIERS: GroupSpec = GroupSpec {
145    count_tag: tag::NO_IOI_QUALIFIERS,
146    delimiter_tag: tag::IOI_QUALIFIER,
147    member_tags: &[tag::IOI_QUALIFIER],
148};
149
150/// NO_ROUTING_IDS (215) — RoutingType is the delimiter tag.
151pub const ROUTING_IDS: GroupSpec = GroupSpec {
152    count_tag: tag::NO_ROUTING_IDS,
153    delimiter_tag: tag::ROUTING_TYPE,
154    member_tags: &[tag::ROUTING_TYPE, tag::ROUTING_ID],
155};
156
157/// NO_MD_ENTRY_TYPES (267) — MDEntryType is the delimiter tag.
158pub const MD_ENTRY_TYPES: GroupSpec = GroupSpec {
159    count_tag: tag::NO_MD_ENTRY_TYPES,
160    delimiter_tag: tag::MD_ENTRY_TYPE,
161    member_tags: &[tag::MD_ENTRY_TYPE],
162};
163
164/// NO_MD_ENTRIES (268) — MDEntryType is the delimiter tag.
165pub const MD_ENTRIES: GroupSpec = GroupSpec {
166    count_tag: tag::NO_MD_ENTRIES,
167    delimiter_tag: tag::MD_ENTRY_TYPE,
168    member_tags: &[
169        tag::MD_ENTRY_TYPE,
170        tag::MD_ENTRY_PX,
171        tag::MD_ENTRY_SIZE,
172        tag::MD_ENTRY_DATE,
173        tag::MD_ENTRY_TIME,
174        tag::TICK_DIRECTION,
175        tag::MD_MKT,
176        tag::QUOTE_CONDITION,
177        tag::TRADE_CONDITION,
178        tag::MD_ENTRY_ID,
179        tag::MD_UPDATE_ACTION,
180        tag::MD_ENTRY_REF_ID,
181        tag::MD_ENTRY_ORIGINATOR,
182        tag::LOCATION_ID,
183        tag::DESK_ID,
184        tag::OPEN_CLOSE_SETTLE_FLAG,
185        tag::SELLER_DAYS,
186        tag::MD_ENTRY_BUYER,
187        tag::MD_ENTRY_SELLER,
188        tag::MD_ENTRY_POSITION_NO,
189        tag::FINANCIAL_STATUS,
190        tag::CORPORATE_ACTION,
191    ],
192};
193
194/// NO_QUOTE_ENTRIES (295) — QuoteEntryID is the delimiter tag.
195pub const QUOTE_ENTRIES: GroupSpec = GroupSpec {
196    count_tag: tag::NO_QUOTE_ENTRIES,
197    delimiter_tag: tag::QUOTE_ENTRY_ID,
198    member_tags: &[
199        tag::QUOTE_ENTRY_ID,
200        tag::SYMBOL,
201        tag::SYMBOL_SFX,
202        tag::SECURITY_ID,
203        tag::ID_SOURCE,
204        tag::SECURITY_TYPE,
205        tag::MATURITY_MONTH_YEAR,
206        tag::MATURITY_DAY,
207        tag::PUT_OR_CALL,
208        tag::STRIKE_PRICE,
209        tag::OPT_ATTRIBUTE,
210        tag::CONTRACT_MULTIPLIER,
211        tag::COUPON_RATE,
212        tag::SECURITY_EXCHANGE,
213        tag::ISSUER,
214        tag::SECURITY_DESC,
215        tag::BID_PX,
216        tag::OFFER_PX,
217        tag::BID_SIZE,
218        tag::OFFER_SIZE,
219        tag::VALID_UNTIL_TIME,
220        tag::BID_SPOT_RATE,
221        tag::OFFER_SPOT_RATE,
222        tag::BID_FORWARD_POINTS,
223        tag::OFFER_FORWARD_POINTS,
224        tag::TRANSACT_TIME,
225        tag::TRADING_SESSION_ID,
226        tag::QUOTE_ENTRY_REJECT_REASON,
227    ],
228};
229
230/// NO_QUOTE_SETS (296) — QuoteSetID is the delimiter tag.
231pub const QUOTE_SETS: GroupSpec = GroupSpec {
232    count_tag: tag::NO_QUOTE_SETS,
233    delimiter_tag: tag::QUOTE_SET_ID,
234    member_tags: &[
235        tag::QUOTE_SET_ID,
236        tag::UNDERLYING_SYMBOL,
237        tag::UNDERLYING_SYMBOL_SFX,
238        tag::UNDERLYING_SECURITY_ID,
239        tag::UNDERLYING_ID_SOURCE,
240        tag::UNDERLYING_SECURITY_TYPE,
241        tag::UNDERLYING_MATURITY_MONTH_YEAR,
242        tag::UNDERLYING_MATURITY_DAY,
243        tag::UNDERLYING_PUT_OR_CALL,
244        tag::UNDERLYING_STRIKE_PRICE,
245        tag::UNDERLYING_OPT_ATTRIBUTE,
246        tag::UNDERLYING_CURRENCY,
247        tag::QUOTE_SET_VALID_UNTIL_TIME,
248        tag::TOT_QUOTE_ENTRIES,
249        tag::NO_QUOTE_ENTRIES,
250    ],
251};
252
253/// NO_CONTRA_BROKERS (382) — ContraBroker is the delimiter tag.
254pub const CONTRA_BROKERS: GroupSpec = GroupSpec {
255    count_tag: tag::NO_CONTRA_BROKERS,
256    delimiter_tag: tag::CONTRA_BROKER,
257    member_tags: &[
258        tag::CONTRA_BROKER,
259        tag::CONTRA_TRADER,
260        tag::CONTRA_TRADE_QTY,
261        tag::CONTRA_TRADE_TIME,
262    ],
263};
264
265/// NO_MSG_TYPES (384) — RefMsgType is the delimiter tag.
266pub const MSG_TYPES: GroupSpec = GroupSpec {
267    count_tag: tag::NO_MSG_TYPES,
268    delimiter_tag: tag::REF_MSG_TYPE,
269    member_tags: &[tag::REF_MSG_TYPE, tag::MSG_DIRECTION],
270};
271
272/// NO_TRADING_SESSIONS (386) — TradingSessionID is the delimiter tag.
273pub const TRADING_SESSIONS: GroupSpec = GroupSpec {
274    count_tag: tag::NO_TRADING_SESSIONS,
275    delimiter_tag: tag::TRADING_SESSION_ID,
276    member_tags: &[tag::TRADING_SESSION_ID],
277};
278
279/// NO_BID_DESCRIPTORS (398) — BidDescriptorType is the delimiter tag.
280pub const BID_DESCRIPTORS: GroupSpec = GroupSpec {
281    count_tag: tag::NO_BID_DESCRIPTORS,
282    delimiter_tag: tag::BID_DESCRIPTOR_TYPE,
283    member_tags: &[
284        tag::BID_DESCRIPTOR_TYPE,
285        tag::BID_DESCRIPTOR,
286        tag::SIDE_VALUE_IND,
287        tag::LIQUIDITY_VALUE,
288        tag::LIQUIDITY_NUM_SECURITIES,
289        tag::LIQUIDITY_PCT_LOW,
290        tag::LIQUIDITY_PCT_HIGH,
291        tag::EFP_TRACKING_ERROR,
292        tag::FAIR_VALUE,
293        tag::OUTSIDE_INDEX_PCT,
294        tag::VALUE_OF_FUTURES,
295    ],
296};
297
298/// NO_BID_COMPONENTS (420) — ClearingFirm is the delimiter tag.
299pub const BID_COMPONENTS: GroupSpec = GroupSpec {
300    count_tag: tag::NO_BID_COMPONENTS,
301    delimiter_tag: tag::CLEARING_FIRM,
302    member_tags: &[
303        tag::CLEARING_FIRM,
304        tag::CLEARING_ACCOUNT,
305        tag::LIQUIDITY_IND_TYPE,
306        tag::WT_AVERAGE_LIQUIDITY,
307        tag::EXCHANGE_FOR_PHYSICAL,
308        tag::OUT_MAIN_CNTRY_U_INDEX,
309        tag::CROSS_PERCENT,
310        tag::PROG_RPT_REQS,
311        tag::PROG_PERIOD_INTERVAL,
312        tag::INC_TAX_IND,
313        tag::NUM_BIDDERS,
314        tag::TRADE_TYPE,
315        tag::BASIS_PX_TYPE,
316        tag::COUNTRY,
317        tag::SIDE,
318        tag::PRICE,
319        tag::PRICE_TYPE,
320        tag::FAIR_VALUE,
321    ],
322};
323
324/// NO_STRIKES (428) — Symbol is the delimiter tag.
325pub const STRIKES: GroupSpec = GroupSpec {
326    count_tag: tag::NO_STRIKES,
327    delimiter_tag: tag::SYMBOL,
328    member_tags: &[
329        tag::SYMBOL,
330        tag::SYMBOL_SFX,
331        tag::SECURITY_ID,
332        tag::ID_SOURCE,
333        tag::SECURITY_TYPE,
334        tag::MATURITY_MONTH_YEAR,
335        tag::MATURITY_DAY,
336        tag::PUT_OR_CALL,
337        tag::STRIKE_PRICE,
338        tag::OPT_ATTRIBUTE,
339        tag::CONTRACT_MULTIPLIER,
340        tag::COUPON_RATE,
341        tag::SECURITY_EXCHANGE,
342        tag::ISSUER,
343        tag::SECURITY_DESC,
344    ],
345};
346
347// ---------------------------------------------------------------------------
348// FIX 4.4 built-in group specs
349// Source: https://www.onixs.biz/fix-dictionary/4.4/
350// ---------------------------------------------------------------------------
351
352/// NO_PARTY_IDS (453) — PartyID is the delimiter tag.
353pub const PARTY_IDS: GroupSpec = GroupSpec {
354    count_tag: tag::NO_PARTY_IDS,
355    delimiter_tag: tag::PARTY_ID,
356    member_tags: &[
357        tag::PARTY_ID,
358        tag::PARTY_ID_SOURCE,
359        tag::PARTY_ROLE,
360        tag::PARTY_SUB_ID,
361    ],
362};
363
364/// NO_SECURITY_ALT_ID (454) — SecurityAltID is the delimiter tag.
365pub const SECURITY_ALT_IDS: GroupSpec = GroupSpec {
366    count_tag: tag::NO_SECURITY_ALT_ID,
367    delimiter_tag: tag::SECURITY_ALT_ID,
368    member_tags: &[tag::SECURITY_ALT_ID, tag::SECURITY_ALT_ID_SOURCE],
369};
370
371/// NO_UNDERLYING_SECURITY_ALT_ID (457) — UnderlyingSecurityAltID is the delimiter tag.
372pub const UNDERLYING_SECURITY_ALT_IDS: GroupSpec = GroupSpec {
373    count_tag: tag::NO_UNDERLYING_SECURITY_ALT_ID,
374    delimiter_tag: tag::UNDERLYING_SECURITY_ALT_ID,
375    member_tags: &[
376        tag::UNDERLYING_SECURITY_ALT_ID,
377        tag::UNDERLYING_SECURITY_ALT_ID_SOURCE,
378    ],
379};
380
381/// NO_REGIST_DTLS (473) — MailingDtls is the delimiter tag.
382pub const REGIST_DTLS: GroupSpec = GroupSpec {
383    count_tag: tag::NO_REGIST_DTLS,
384    delimiter_tag: tag::MAILING_DTLS,
385    member_tags: &[
386        tag::MAILING_DTLS,
387        tag::INVESTOR_COUNTRY_OF_RESIDENCE,
388        tag::MAILING_INST,
389        tag::REGIST_DTLS,
390        tag::REGIST_EMAIL,
391        tag::DISTRIB_PERCENTAGE,
392        tag::REGIST_ID,
393        tag::REGIST_TRANS_TYPE,
394        tag::OWNER_TYPE,
395        tag::NO_DISTRIB_INSTS,
396        tag::DISTRIB_PAYMENT_METHOD,
397        tag::CASH_DISTRIB_CURR,
398        tag::CASH_DISTRIB_AGENT_NAME,
399        tag::CASH_DISTRIB_AGENT_CODE,
400        tag::CASH_DISTRIB_AGENT_ACCT_NUMBER,
401        tag::CASH_DISTRIB_PAY_REF,
402        tag::CASH_DISTRIB_AGENT_ACCT_NAME,
403    ],
404};
405
406/// NO_DISTRIB_INSTS (510) — DistribPaymentMethod is the delimiter tag.
407pub const DISTRIB_INSTS: GroupSpec = GroupSpec {
408    count_tag: tag::NO_DISTRIB_INSTS,
409    delimiter_tag: tag::DISTRIB_PAYMENT_METHOD,
410    member_tags: &[
411        tag::DISTRIB_PAYMENT_METHOD,
412        tag::DISTRIB_PERCENTAGE,
413        tag::CASH_DISTRIB_CURR,
414        tag::CASH_DISTRIB_AGENT_NAME,
415        tag::CASH_DISTRIB_AGENT_CODE,
416        tag::CASH_DISTRIB_AGENT_ACCT_NUMBER,
417        tag::CASH_DISTRIB_PAY_REF,
418        tag::CASH_DISTRIB_AGENT_ACCT_NAME,
419    ],
420};
421
422/// NO_CONT_AMTS (518) — ContAmtType is the delimiter tag.
423pub const CONT_AMTS: GroupSpec = GroupSpec {
424    count_tag: tag::NO_CONT_AMTS,
425    delimiter_tag: tag::CONT_AMT_TYPE,
426    member_tags: &[tag::CONT_AMT_TYPE, tag::CONT_AMT_VALUE, tag::CONT_AMT_CURR],
427};
428
429/// NO_NESTED_PARTY_IDS (539) — NestedPartyID is the delimiter tag.
430pub const NESTED_PARTY_IDS: GroupSpec = GroupSpec {
431    count_tag: tag::NO_NESTED_PARTY_IDS,
432    delimiter_tag: tag::NESTED_PARTY_ID,
433    member_tags: &[
434        tag::NESTED_PARTY_ID,
435        tag::NESTED_PARTY_ID_SOURCE,
436        tag::NESTED_PARTY_ROLE,
437        tag::NESTED_PARTY_SUB_ID,
438    ],
439};
440
441/// NO_SIDES (552) — Side is the delimiter tag.
442pub const SIDES: GroupSpec = GroupSpec {
443    count_tag: tag::NO_SIDES,
444    delimiter_tag: tag::SIDE,
445    member_tags: &[
446        tag::SIDE,
447        tag::ORDER_ID,
448        tag::SECONDARY_ORDER_ID,
449        tag::CL_ORD_ID,
450        tag::SECONDARY_CL_ORD_ID,
451        tag::LIST_ID,
452        tag::ACCOUNT,
453        tag::ACCT_ID_SOURCE,
454        tag::ACCOUNT_TYPE,
455        tag::PROCESS_CODE,
456        tag::ODD_LOT,
457        tag::NO_CLEARING_INSTRUCTIONS,
458        tag::CLEARING_INSTRUCTION,
459        tag::CLEARING_FEE_INDICATOR,
460        tag::TRADE_INPUT_SOURCE,
461        tag::TRADE_INPUT_DEVICE,
462        tag::ORDER_INPUT_DEVICE,
463        tag::CURRENCY,
464        tag::COMPLIANCE_ID,
465        tag::SOLICITED_FLAG,
466        tag::ORDER_CAPACITY,
467        tag::ORDER_RESTRICTIONS,
468        tag::CUST_ORDER_CAPACITY,
469        tag::ORD_TYPE,
470        tag::EXEC_INST,
471        tag::TRANS_BKD_TIME,
472        tag::TRADING_SESSION_ID,
473        tag::TRADING_SESSION_SUB_ID,
474        tag::COMMISSION,
475        tag::COMM_TYPE,
476        tag::COMM_CURRENCY,
477        tag::FUND_RENEW_WAIV,
478        tag::GROSS_TRADE_AMT,
479        tag::NUM_DAYS_INTEREST,
480        tag::EX_DESTINATION,
481        tag::ACCRUED_INTEREST_RATE,
482        tag::ACCRUED_INTEREST_AMT,
483        tag::INTEREST_AT_MATURITY,
484        tag::END_ACCRUED_INTEREST_AMT,
485        tag::START_CASH,
486        tag::END_CASH,
487        tag::NET_MONEY,
488        tag::SETTL_CURR_AMT,
489        tag::SETTL_CURRENCY,
490        tag::SETTL_CURR_FX_RATE,
491        tag::SETTL_CURR_FX_RATE_CALC,
492        tag::POSITION_EFFECT,
493        tag::TEXT,
494        tag::ENCODED_TEXT_LEN,
495        tag::ENCODED_TEXT,
496        tag::SIDE_MULTI_LEG_REPORTING_TYPE,
497        tag::NO_CONT_AMTS,
498        tag::CONT_AMT_TYPE,
499        tag::CONT_AMT_VALUE,
500        tag::CONT_AMT_CURR,
501        tag::NO_MISC_FEES,
502        tag::MISC_FEE_AMT,
503        tag::MISC_FEE_CURR,
504        tag::MISC_FEE_TYPE,
505        tag::MISC_FEE_BASIS,
506        tag::EXCHANGE_RULE,
507        tag::TRADE_ALLOC_INDICATOR,
508        tag::PREALLOC_METHOD,
509        tag::ALLOC_ID,
510        tag::NO_ALLOCS,
511        tag::ALLOC_ACCOUNT,
512        tag::ALLOC_ACCT_ID_SOURCE,
513        tag::ALLOC_SETTL_CURRENCY,
514        tag::INDIVIDUAL_ALLOC_ID,
515        tag::ALLOC_SHARES,
516    ],
517};
518
519/// NO_SECURITY_TYPES (558) — SecurityType is the delimiter tag.
520pub const SECURITY_TYPES: GroupSpec = GroupSpec {
521    count_tag: tag::NO_SECURITY_TYPES,
522    delimiter_tag: tag::SECURITY_TYPE,
523    member_tags: &[tag::SECURITY_TYPE, tag::PRODUCT, tag::CFI_CODE],
524};
525
526/// NO_AFFECTED_ORDERS (534) — AffectedOrderID is the delimiter tag.
527pub const AFFECTED_ORDERS: GroupSpec = GroupSpec {
528    count_tag: tag::NO_AFFECTED_ORDERS,
529    delimiter_tag: tag::AFFECTED_ORDER_ID,
530    member_tags: &[tag::AFFECTED_ORDER_ID, tag::AFFECTED_SECONDARY_ORDER_ID],
531};
532
533/// NO_LEGS (555) — LegSymbol is the delimiter tag.
534pub const LEGS: GroupSpec = GroupSpec {
535    count_tag: tag::NO_LEGS,
536    delimiter_tag: tag::LEG_SYMBOL,
537    member_tags: &[
538        tag::LEG_SYMBOL,
539        tag::LEG_SYMBOL_SFX,
540        tag::LEG_SECURITY_ID,
541        tag::LEG_SECURITY_ID_SOURCE,
542        tag::NO_LEG_SECURITY_ALT_ID,
543        tag::LEG_SECURITY_ALT_ID,
544        tag::LEG_SECURITY_ALT_ID_SOURCE,
545        tag::LEG_PRODUCT,
546        tag::LEG_CFI_CODE,
547        tag::LEG_SECURITY_TYPE,
548        tag::LEG_MATURITY_MONTH_YEAR,
549        tag::LEG_MATURITY_DATE,
550        tag::LEG_STRIKE_PRICE,
551        tag::LEG_OPT_ATTRIBUTE,
552        tag::LEG_CONTRACT_MULTIPLIER,
553        tag::LEG_COUPON_RATE,
554        tag::LEG_SECURITY_EXCHANGE,
555        tag::LEG_ISSUER,
556        tag::ENCODED_LEG_ISSUER_LEN,
557        tag::ENCODED_LEG_ISSUER,
558        tag::LEG_SECURITY_DESC,
559        tag::ENCODED_LEG_SECURITY_DESC_LEN,
560        tag::ENCODED_LEG_SECURITY_DESC,
561        tag::LEG_RATIO_QTY,
562        tag::LEG_SIDE,
563        tag::LEG_CURRENCY,
564        tag::LEG_COUNTRY_OF_ISSUE,
565        tag::LEG_STATE_OR_PROVINCE_OF_ISSUE,
566        tag::LEG_LOCALE_OF_ISSUE,
567        tag::LEG_INSTR_REGISTRY,
568        tag::LEG_DATED_DATE,
569        tag::LEG_POOL,
570        tag::LEG_CONTRACT_SETTL_MONTH,
571        tag::LEG_INTEREST_ACCRUAL_DATE,
572        tag::LEG_QTY,
573        tag::LEG_SWAP_TYPE,
574        tag::NO_LEG_STIPULATIONS,
575        tag::LEG_STIPULATION_TYPE,
576        tag::LEG_STIPULATION_VALUE,
577        tag::LEG_POSITION_EFFECT,
578        tag::LEG_COVERED_OR_UNCOVERED,
579        tag::LEG_PRICE,
580        tag::LEG_SETTL_TYPE,
581        tag::LEG_SETTL_DATE,
582        tag::LEG_LAST_PX,
583        tag::LEG_REF_ID,
584    ],
585};
586
587/// NO_UNDERLYINGS (711) — UnderlyingSymbol is the delimiter tag.
588pub const UNDERLYINGS: GroupSpec = GroupSpec {
589    count_tag: tag::NO_UNDERLYINGS,
590    delimiter_tag: tag::UNDERLYING_SYMBOL,
591    member_tags: &[
592        tag::UNDERLYING_SYMBOL,
593        tag::UNDERLYING_SYMBOL_SFX,
594        tag::UNDERLYING_SECURITY_ID,
595        tag::UNDERLYING_ID_SOURCE,
596        tag::UNDERLYING_SECURITY_TYPE,
597        tag::UNDERLYING_MATURITY_MONTH_YEAR,
598        tag::UNDERLYING_MATURITY_DATE,
599        tag::UNDERLYING_PUT_OR_CALL,
600        tag::UNDERLYING_STRIKE_PRICE,
601        tag::UNDERLYING_OPT_ATTRIBUTE,
602        tag::UNDERLYING_CONTRACT_MULTIPLIER,
603        tag::UNDERLYING_COUPON_RATE,
604        tag::UNDERLYING_SECURITY_EXCHANGE,
605        tag::UNDERLYING_ISSUER,
606        tag::ENCODED_UNDERLYING_ISSUER_LEN,
607        tag::ENCODED_UNDERLYING_ISSUER,
608        tag::UNDERLYING_SECURITY_DESC,
609        tag::ENCODED_UNDERLYING_SECURITY_DESC_LEN,
610        tag::ENCODED_UNDERLYING_SECURITY_DESC,
611        tag::UNDERLYING_COUPON_PAYMENT_DATE,
612        tag::UNDERLYING_ISSUE_DATE,
613        tag::UNDERLYING_REPO_COLLATERAL_SECURITY_TYPE,
614        tag::UNDERLYING_REPURCHASE_TERM,
615        tag::UNDERLYING_REPURCHASE_RATE,
616        tag::UNDERLYING_FACTOR,
617        tag::UNDERLYING_CREDIT_RATING,
618        tag::UNDERLYING_INSTR_REGISTRY,
619        tag::UNDERLYING_COUNTRY_OF_ISSUE,
620        tag::UNDERLYING_STATE_OR_PROVINCE_OF_ISSUE,
621        tag::UNDERLYING_LOCALE_OF_ISSUE,
622        tag::UNDERLYING_REDEMPTION_DATE,
623        tag::UNDERLYING_STRIKE_CURRENCY,
624        tag::UNDERLYING_SECURITY_SUB_TYPE,
625        tag::UNDERLYING_PRODUCT,
626        tag::UNDERLYING_CFI_CODE,
627        tag::UNDERLYING_CP_PROGRAM,
628        tag::UNDERLYING_CP_REG_TYPE,
629        tag::UNDERLYING_LAST_PX,
630        tag::UNDERLYING_LAST_QTY,
631        tag::UNDERLYING_QTY,
632        tag::UNDERLYING_SETTL_PRICE,
633        tag::UNDERLYING_SETTL_PRICE_TYPE,
634        tag::UNDERLYING_DIRTY_PRICE,
635        tag::UNDERLYING_END_PRICE,
636        tag::UNDERLYING_START_VALUE,
637        tag::UNDERLYING_CURRENT_VALUE,
638        tag::UNDERLYING_END_VALUE,
639        tag::NO_UNDERLYING_SECURITY_ALT_ID,
640        tag::UNDERLYING_SECURITY_ALT_ID,
641        tag::UNDERLYING_SECURITY_ALT_ID_SOURCE,
642        tag::UNDERLYING_STIP_TYPE,
643        tag::UNDERLYING_STIP_VALUE,
644    ],
645};
646
647/// NO_POSITIONS (702) — PosType is the delimiter tag.
648pub const POSITIONS: GroupSpec = GroupSpec {
649    count_tag: tag::NO_POSITIONS,
650    delimiter_tag: tag::POS_TYPE,
651    member_tags: &[
652        tag::POS_TYPE,
653        tag::LONG_QTY,
654        tag::SHORT_QTY,
655        tag::POS_QTY_STATUS,
656    ],
657};
658
659/// NO_QUOTE_QUALIFIERS (735) — QuoteQualifier is the delimiter tag.
660pub const QUOTE_QUALIFIERS: GroupSpec = GroupSpec {
661    count_tag: tag::NO_QUOTE_QUALIFIERS,
662    delimiter_tag: tag::QUOTE_QUALIFIER,
663    member_tags: &[tag::QUOTE_QUALIFIER],
664};
665
666/// NO_POS_AMT (753) — PosAmtType is the delimiter tag.
667pub const POS_AMTS: GroupSpec = GroupSpec {
668    count_tag: tag::NO_POS_AMT,
669    delimiter_tag: tag::POS_AMT_TYPE,
670    member_tags: &[tag::POS_AMT_TYPE, tag::POS_AMT],
671};
672
673/// NO_NESTED2_PARTY_IDS (756) — Nested2PartyID is the delimiter tag.
674pub const NESTED2_PARTY_IDS: GroupSpec = GroupSpec {
675    count_tag: tag::NO_NESTED2_PARTY_IDS,
676    delimiter_tag: tag::NESTED2_PARTY_ID,
677    member_tags: &[
678        tag::NESTED2_PARTY_ID,
679        tag::NESTED2_PARTY_ID_SOURCE,
680        tag::NESTED2_PARTY_ROLE,
681        tag::NESTED2_PARTY_SUB_ID,
682    ],
683};
684
685/// NO_TRD_REG_TIMESTAMPS (768) — TrdRegTimestamp is the delimiter tag.
686pub const TRD_REG_TIMESTAMPS: GroupSpec = GroupSpec {
687    count_tag: tag::NO_TRD_REG_TIMESTAMPS,
688    delimiter_tag: tag::TRD_REG_TIMESTAMP,
689    member_tags: &[
690        tag::TRD_REG_TIMESTAMP,
691        tag::TRD_REG_TIMESTAMP_TYPE,
692        tag::TRD_REG_TIMESTAMP_ORIGIN,
693    ],
694};
695
696/// NO_SETTL_INST (778) — SettlInstID is the delimiter tag.
697pub const SETTL_INST: GroupSpec = GroupSpec {
698    count_tag: tag::NO_SETTL_INST,
699    delimiter_tag: tag::SETTL_INST_ID,
700    member_tags: &[
701        tag::SETTL_INST_ID,
702        tag::SETTL_INST_TRANS_TYPE,
703        tag::SETTL_INST_REF_ID,
704        tag::SETTL_INST_MODE,
705        tag::SETTL_INST_SOURCE,
706        tag::SECURITY_ID,
707        tag::SIDE,
708        tag::TRANSACT_TIME,
709        tag::EFFECTIVE_TIME,
710    ],
711};
712
713/// NO_SETTL_PARTY_IDS (781) — SettlPartyID is the delimiter tag.
714pub const SETTL_PARTY_IDS: GroupSpec = GroupSpec {
715    count_tag: tag::NO_SETTL_PARTY_IDS,
716    delimiter_tag: tag::SETTL_PARTY_ID,
717    member_tags: &[
718        tag::SETTL_PARTY_ID,
719        tag::SETTL_PARTY_ID_SOURCE,
720        tag::SETTL_PARTY_ROLE,
721        tag::SETTL_PARTY_SUB_ID,
722        tag::SETTL_PARTY_SUB_ID_TYPE,
723    ],
724};
725
726/// NO_PARTY_SUB_IDS (802) — PartySubID is the delimiter tag.
727pub const PARTY_SUB_IDS: GroupSpec = GroupSpec {
728    count_tag: tag::NO_PARTY_SUB_IDS,
729    delimiter_tag: tag::PARTY_SUB_ID,
730    member_tags: &[tag::PARTY_SUB_ID, tag::PARTY_SUB_ID_TYPE],
731};
732
733/// NO_NESTED_PARTY_SUB_IDS (804) — NestedPartySubID is the delimiter tag.
734pub const NESTED_PARTY_SUB_IDS: GroupSpec = GroupSpec {
735    count_tag: tag::NO_NESTED_PARTY_SUB_IDS,
736    delimiter_tag: tag::NESTED_PARTY_SUB_ID,
737    member_tags: &[tag::NESTED_PARTY_SUB_ID, tag::NESTED_PARTY_SUB_ID_TYPE],
738};
739
740/// NO_NESTED2_PARTY_SUB_IDS (806) — Nested2PartySubID is the delimiter tag.
741pub const NESTED2_PARTY_SUB_IDS: GroupSpec = GroupSpec {
742    count_tag: tag::NO_NESTED2_PARTY_SUB_IDS,
743    delimiter_tag: tag::NESTED2_PARTY_SUB_ID,
744    member_tags: &[tag::NESTED2_PARTY_SUB_ID, tag::NESTED2_PARTY_SUB_ID_TYPE],
745};
746
747/// NO_ALT_MD_SOURCE (816) — AltMDSourceID is the delimiter tag.
748pub const ALT_MD_SOURCES: GroupSpec = GroupSpec {
749    count_tag: tag::NO_ALT_MD_SOURCE,
750    delimiter_tag: tag::ALT_MD_SOURCE_ID,
751    member_tags: &[tag::ALT_MD_SOURCE_ID],
752};
753
754/// NO_CAPACITIES (862) — OrderCapacity is the delimiter tag.
755pub const CAPACITIES: GroupSpec = GroupSpec {
756    count_tag: tag::NO_CAPACITIES,
757    delimiter_tag: tag::ORDER_CAPACITY,
758    member_tags: &[tag::ORDER_CAPACITY, tag::ORDER_CAPACITY_QTY],
759};
760
761/// NO_EVENTS (864) — EventType is the delimiter tag.
762pub const EVENTS: GroupSpec = GroupSpec {
763    count_tag: tag::NO_EVENTS,
764    delimiter_tag: tag::EVENT_TYPE,
765    member_tags: &[
766        tag::EVENT_TYPE,
767        tag::EVENT_DATE,
768        tag::EVENT_PX,
769        tag::EVENT_TEXT,
770    ],
771};
772
773/// NO_INSTR_ATTRIB (870) — InstrAttribType is the delimiter tag.
774pub const INSTR_ATTRIB: GroupSpec = GroupSpec {
775    count_tag: tag::NO_INSTR_ATTRIB,
776    delimiter_tag: tag::INSTR_ATTRIB_TYPE,
777    member_tags: &[tag::INSTR_ATTRIB_TYPE, tag::INSTR_ATTRIB_VALUE],
778};
779
780/// NO_UNDERLYING_STIPS (887) — UnderlyingStipType is the delimiter tag.
781pub const UNDERLYING_STIPS: GroupSpec = GroupSpec {
782    count_tag: tag::NO_UNDERLYING_STIPS,
783    delimiter_tag: tag::UNDERLYING_STIP_TYPE,
784    member_tags: &[tag::UNDERLYING_STIP_TYPE, tag::UNDERLYING_STIP_VALUE],
785};
786
787/// NO_TRADES (897) — TradeReportID is the delimiter tag.
788pub const TRADES: GroupSpec = GroupSpec {
789    count_tag: tag::NO_TRADES,
790    delimiter_tag: tag::TRADE_REPORT_ID,
791    member_tags: &[tag::TRADE_REPORT_ID, tag::SECONDARY_TRADE_REPORT_ID],
792};
793
794/// NO_COMP_IDS (936) — RefCompID is the delimiter tag.
795pub const COMP_IDS: GroupSpec = GroupSpec {
796    count_tag: tag::NO_COMP_IDS,
797    delimiter_tag: tag::REF_COMP_ID,
798    member_tags: &[
799        tag::REF_COMP_ID,
800        tag::REF_SUB_ID,
801        tag::STATUS_VALUE,
802        tag::STATUS_TEXT,
803    ],
804};
805
806/// NO_COLL_INQUIRY_QUALIFIER (938) — CollInquiryQualifier is the delimiter tag.
807pub const COLL_INQUIRY_QUALIFIERS: GroupSpec = GroupSpec {
808    count_tag: tag::NO_COLL_INQUIRY_QUALIFIER,
809    delimiter_tag: tag::COLL_INQUIRY_QUALIFIER,
810    member_tags: &[tag::COLL_INQUIRY_QUALIFIER],
811};
812
813/// NO_NESTED3_PARTY_IDS (948) — Nested3PartyID is the delimiter tag.
814pub const NESTED3_PARTY_IDS: GroupSpec = GroupSpec {
815    count_tag: tag::NO_NESTED3_PARTY_IDS,
816    delimiter_tag: tag::NESTED3_PARTY_ID,
817    member_tags: &[
818        tag::NESTED3_PARTY_ID,
819        tag::NESTED3_PARTY_ID_SOURCE,
820        tag::NESTED3_PARTY_ROLE,
821        tag::NESTED3_PARTY_SUB_ID,
822        tag::NESTED3_PARTY_SUB_ID_TYPE,
823    ],
824};
825
826/// NO_LEG_SECURITY_ALT_ID (604) — LegSecurityAltID is the delimiter tag.
827pub const LEG_SECURITY_ALT_IDS: GroupSpec = GroupSpec {
828    count_tag: tag::NO_LEG_SECURITY_ALT_ID,
829    delimiter_tag: tag::LEG_SECURITY_ALT_ID,
830    member_tags: &[tag::LEG_SECURITY_ALT_ID, tag::LEG_SECURITY_ALT_ID_SOURCE],
831};
832
833/// NO_LEG_STIPULATIONS (683) — LegStipulationType is the delimiter tag.
834pub const LEG_STIPULATIONS: GroupSpec = GroupSpec {
835    count_tag: tag::NO_LEG_STIPULATIONS,
836    delimiter_tag: tag::LEG_STIPULATION_TYPE,
837    member_tags: &[tag::LEG_STIPULATION_TYPE, tag::LEG_STIPULATION_VALUE],
838};
839
840/// NO_LEG_ALLOCS (670) — LegAllocAccount is the delimiter tag.
841pub const LEG_ALLOCS: GroupSpec = GroupSpec {
842    count_tag: tag::NO_LEG_ALLOCS,
843    delimiter_tag: tag::LEG_ALLOC_ACCOUNT,
844    member_tags: &[
845        tag::LEG_ALLOC_ACCOUNT,
846        tag::LEG_INDIVIDUAL_ALLOC_ID,
847        tag::LEG_ALLOC_QTY,
848        tag::LEG_ALLOC_ACCT_ID_SOURCE,
849        tag::LEG_SETTL_CURRENCY,
850    ],
851};
852
853/// NO_HOPS (627) — HopCompID is the delimiter tag.
854pub const HOPS: GroupSpec = GroupSpec {
855    count_tag: tag::NO_HOPS,
856    delimiter_tag: tag::HOP_COMP_ID,
857    member_tags: &[tag::HOP_COMP_ID, tag::HOP_SENDING_TIME, tag::HOP_REF_ID],
858};
859
860/// NO_CLEARING_INSTRUCTIONS (576) — ClearingInstruction is the delimiter tag.
861pub const CLEARING_INSTRUCTIONS: GroupSpec = GroupSpec {
862    count_tag: tag::NO_CLEARING_INSTRUCTIONS,
863    delimiter_tag: tag::CLEARING_INSTRUCTION,
864    member_tags: &[tag::CLEARING_INSTRUCTION],
865};
866
867/// All built-in FIX 4.4 group specs (superset of `FIX42_GROUPS`).
868///
869/// Includes all FIX 4.2 groups plus the groups introduced in FIX 4.4,
870/// so this array alone covers every repeating group that can appear in
871/// a FIX 4.4 message.
872pub const FIX44_GROUPS: &[&GroupSpec] = &[
873    // -- FIX 4.2 groups (inherited) --
874    &ALLOCS,
875    &ORDERS,
876    &RPTS,
877    &DLVY_INST,
878    &EXECS,
879    &MISC_FEES,
880    &RELATED_SYM,
881    &IOI_QUALIFIERS,
882    &ROUTING_IDS,
883    &MD_ENTRY_TYPES,
884    &MD_ENTRIES,
885    &QUOTE_ENTRIES,
886    &QUOTE_SETS,
887    &CONTRA_BROKERS,
888    &MSG_TYPES,
889    &TRADING_SESSIONS,
890    &BID_DESCRIPTORS,
891    &BID_COMPONENTS,
892    &STRIKES,
893    // -- FIX 4.4 additions --
894    &PARTY_IDS,
895    &SECURITY_ALT_IDS,
896    &UNDERLYING_SECURITY_ALT_IDS,
897    &REGIST_DTLS,
898    &DISTRIB_INSTS,
899    &CONT_AMTS,
900    &NESTED_PARTY_IDS,
901    &SIDES,
902    &SECURITY_TYPES,
903    &AFFECTED_ORDERS,
904    &LEGS,
905    &UNDERLYINGS,
906    &POSITIONS,
907    &QUOTE_QUALIFIERS,
908    &POS_AMTS,
909    &NESTED2_PARTY_IDS,
910    &TRD_REG_TIMESTAMPS,
911    &SETTL_INST,
912    &SETTL_PARTY_IDS,
913    &PARTY_SUB_IDS,
914    &NESTED_PARTY_SUB_IDS,
915    &NESTED2_PARTY_SUB_IDS,
916    &ALT_MD_SOURCES,
917    &CAPACITIES,
918    &EVENTS,
919    &INSTR_ATTRIB,
920    &UNDERLYING_STIPS,
921    &TRADES,
922    &COMP_IDS,
923    &COLL_INQUIRY_QUALIFIERS,
924    &NESTED3_PARTY_IDS,
925    &LEG_SECURITY_ALT_IDS,
926    &LEG_STIPULATIONS,
927    &LEG_ALLOCS,
928    &HOPS,
929    &CLEARING_INSTRUCTIONS,
930];
931
932/// All built-in FIX 4.2 group specs.
933pub const FIX42_GROUPS: &[&GroupSpec] = &[
934    &ALLOCS,
935    &ORDERS,
936    &RPTS,
937    &DLVY_INST,
938    &EXECS,
939    &MISC_FEES,
940    &RELATED_SYM,
941    &IOI_QUALIFIERS,
942    &ROUTING_IDS,
943    &MD_ENTRY_TYPES,
944    &MD_ENTRIES,
945    &QUOTE_ENTRIES,
946    &QUOTE_SETS,
947    &CONTRA_BROKERS,
948    &MSG_TYPES,
949    &TRADING_SESSIONS,
950    &BID_DESCRIPTORS,
951    &BID_COMPONENTS,
952    &STRIKES,
953];
954
955// ---------------------------------------------------------------------------
956// Group and GroupIter
957// ---------------------------------------------------------------------------
958
959/// A zero-copy view over one repeating-group instance.
960///
961/// Borrows a sub-slice of the parent `Message`'s offset array and the raw
962/// input buffer. Field access is identical to `Message`: zero allocation,
963/// zero copy.
964#[derive(Debug, Clone, Copy)]
965pub struct Group<'a> {
966    pub(crate) buf: &'a [u8],
967    pub(crate) offsets: &'a [(Tag, u32, u32)],
968}
969
970impl<'a> Group<'a> {
971    /// Number of fields in this group instance.
972    #[inline]
973    pub fn len(&self) -> usize {
974        self.offsets.len()
975    }
976
977    /// Returns true if this group instance contains no fields.
978    #[inline]
979    pub fn is_empty(&self) -> bool {
980        self.offsets.is_empty()
981    }
982
983    /// Returns the field at `index`. Panics if `index >= self.len()`.
984    #[inline]
985    pub fn field(&self, index: usize) -> Field<'a> {
986        let (tag, start, end) = self.offsets[index];
987        Field {
988            tag,
989            value: &self.buf[start as usize..end as usize],
990        }
991    }
992
993    /// Iterates over all fields in this group instance.
994    #[inline]
995    pub fn fields(&self) -> impl Iterator<Item = Field<'a>> + '_ {
996        self.offsets.iter().map(move |&(tag, start, end)| Field {
997            tag,
998            value: &self.buf[start as usize..end as usize],
999        })
1000    }
1001
1002    /// Returns the first field with the given tag, or `None`.
1003    #[inline]
1004    pub fn find(&self, tag: Tag) -> Option<Field<'a>> {
1005        self.offsets
1006            .iter()
1007            .find(|&&(t, _, _)| t == tag)
1008            .map(|&(t, start, end)| Field {
1009                tag: t,
1010                value: &self.buf[start as usize..end as usize],
1011            })
1012    }
1013
1014    /// Return an iterator over the instances of a repeating group nested inside
1015    /// this group instance. Mirrors [`Message::groups`] exactly.
1016    ///
1017    /// Because this group's `offsets` slice is already bounded to this parent
1018    /// instance, the nested iterator cannot escape into sibling parent instances.
1019    ///
1020    /// Returns an empty iterator if the nested count tag is absent or zero.
1021    #[inline]
1022    pub fn groups(&self, spec: &GroupSpec) -> GroupIter<'a> {
1023        let pos = self
1024            .offsets
1025            .iter()
1026            .position(|&(t, _, _)| t == spec.count_tag);
1027
1028        let (count, remaining) = match pos {
1029            None => (0, &[][..]),
1030            Some(i) => {
1031                let (_, start, end) = self.offsets[i];
1032                let count = parse_count(&self.buf[start as usize..end as usize]);
1033                let after = &self.offsets[i + 1..];
1034                (count, after)
1035            }
1036        };
1037
1038        GroupIter {
1039            buf: self.buf,
1040            remaining,
1041            delimiter_tag: spec.delimiter_tag,
1042            count,
1043            emitted: 0,
1044        }
1045    }
1046}
1047
1048/// Iterator over the instances of one repeating group.
1049///
1050/// Produced by [`Message::groups`]. Each call to `next` returns the next
1051/// `Group` instance as a zero-copy view into the parent message.
1052pub struct GroupIter<'a> {
1053    pub(crate) buf: &'a [u8],
1054    /// Remaining flat offsets starting just after the NO_* count tag.
1055    pub(crate) remaining: &'a [(Tag, u32, u32)],
1056    pub(crate) delimiter_tag: Tag,
1057    pub(crate) count: usize,
1058    pub(crate) emitted: usize,
1059}
1060
1061impl<'a> Iterator for GroupIter<'a> {
1062    type Item = Group<'a>;
1063
1064    fn next(&mut self) -> Option<Group<'a>> {
1065        if self.emitted >= self.count || self.remaining.is_empty() {
1066            return None;
1067        }
1068
1069        let start = self.remaining;
1070
1071        // Find the end of this instance: the next occurrence of the delimiter tag
1072        // after the first field, or the end of remaining.
1073        let end_offset = start
1074            .iter()
1075            .enumerate()
1076            .skip(1) // skip the delimiter tag that begins this instance
1077            .find(|&(_, &(t, _, _))| t == self.delimiter_tag)
1078            .map(|(i, _)| i)
1079            .unwrap_or(start.len());
1080
1081        let instance_offsets = &start[..end_offset];
1082        self.remaining = &start[end_offset..];
1083        self.emitted += 1;
1084
1085        Some(Group {
1086            buf: self.buf,
1087            offsets: instance_offsets,
1088        })
1089    }
1090
1091    fn size_hint(&self) -> (usize, Option<usize>) {
1092        let left = self.count.saturating_sub(self.emitted);
1093        (left, Some(left))
1094    }
1095}
1096
1097// ---------------------------------------------------------------------------
1098// Helpers used by message.rs
1099// ---------------------------------------------------------------------------
1100
1101/// Parse a decimal ASCII count value from raw bytes. Returns 0 on failure.
1102pub(crate) fn parse_count(bytes: &[u8]) -> usize {
1103    let mut n: usize = 0;
1104    for &b in bytes {
1105        if !b.is_ascii_digit() {
1106            return 0;
1107        }
1108        n = n.wrapping_mul(10).wrapping_add((b - b'0') as usize);
1109    }
1110    n
1111}
1112
1113// ---------------------------------------------------------------------------
1114// Tests
1115// ---------------------------------------------------------------------------
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120    use crate::decoder::Decoder;
1121    use crate::tag;
1122
1123    // Helper: build a raw FIX byte string from "tag=value|..." notation using '|' as SOH.
1124    fn fix(s: &str) -> Vec<u8> {
1125        s.bytes()
1126            .map(|b| if b == b'|' { 0x01 } else { b })
1127            .collect()
1128    }
1129
1130    // -----------------------------------------------------------------------
1131    // parse_count
1132    // -----------------------------------------------------------------------
1133
1134    #[test]
1135    fn parse_count_normal() {
1136        assert_eq!(parse_count(b"3"), 3);
1137        assert_eq!(parse_count(b"10"), 10);
1138        assert_eq!(parse_count(b"0"), 0);
1139    }
1140
1141    #[test]
1142    fn parse_count_invalid_returns_zero() {
1143        assert_eq!(parse_count(b""), 0);
1144        assert_eq!(parse_count(b"abc"), 0);
1145        assert_eq!(parse_count(b"1a"), 0);
1146    }
1147
1148    // -----------------------------------------------------------------------
1149    // GroupIter — single group, one instance
1150    // -----------------------------------------------------------------------
1151
1152    #[test]
1153    fn single_group_single_instance() {
1154        // NO_MISC_FEES=1 | MiscFeeAmt=10.5 | MiscFeeCurr=USD | MiscFeeType=1
1155        let raw = fix("136=1|137=10.5|138=USD|139=1|");
1156        let mut dec = Decoder::new();
1157        let msg = dec.decode(&raw).unwrap();
1158
1159        let mut iter = msg.groups(&MISC_FEES);
1160        let g = iter.next().expect("expected one instance");
1161        assert_eq!(g.len(), 3);
1162        assert_eq!(g.field(0).tag, tag::MISC_FEE_AMT);
1163        assert_eq!(g.field(0).value, b"10.5");
1164        assert_eq!(g.field(1).tag, tag::MISC_FEE_CURR);
1165        assert_eq!(g.field(1).value, b"USD");
1166        assert_eq!(g.field(2).tag, tag::MISC_FEE_TYPE);
1167        assert_eq!(g.field(2).value, b"1");
1168        assert!(iter.next().is_none());
1169    }
1170
1171    // -----------------------------------------------------------------------
1172    // GroupIter — single group, multiple instances
1173    // -----------------------------------------------------------------------
1174
1175    #[test]
1176    fn single_group_two_instances() {
1177        // NO_MISC_FEES=2 | instance1 | instance2
1178        let raw = fix("136=2|137=10.5|138=USD|139=1|137=5.0|138=EUR|139=2|");
1179        let mut dec = Decoder::new();
1180        let msg = dec.decode(&raw).unwrap();
1181
1182        let instances: Vec<_> = msg.groups(&MISC_FEES).collect();
1183        assert_eq!(instances.len(), 2);
1184
1185        let g1 = &instances[0];
1186        assert_eq!(g1.find(tag::MISC_FEE_AMT).unwrap().value, b"10.5");
1187        assert_eq!(g1.find(tag::MISC_FEE_CURR).unwrap().value, b"USD");
1188
1189        let g2 = &instances[1];
1190        assert_eq!(g2.find(tag::MISC_FEE_AMT).unwrap().value, b"5.0");
1191        assert_eq!(g2.find(tag::MISC_FEE_CURR).unwrap().value, b"EUR");
1192    }
1193
1194    // -----------------------------------------------------------------------
1195    // GroupIter — count=0 produces empty iterator
1196    // -----------------------------------------------------------------------
1197
1198    #[test]
1199    fn group_count_zero_empty_iter() {
1200        let raw = fix("136=0|35=D|");
1201        let mut dec = Decoder::new();
1202        let msg = dec.decode(&raw).unwrap();
1203
1204        let mut iter = msg.groups(&MISC_FEES);
1205        assert!(iter.next().is_none());
1206    }
1207
1208    // -----------------------------------------------------------------------
1209    // GroupIter — count tag absent produces empty iterator
1210    // -----------------------------------------------------------------------
1211
1212    #[test]
1213    fn group_count_tag_absent_empty_iter() {
1214        let raw = fix("35=D|49=SENDER|56=TARGET|");
1215        let mut dec = Decoder::new();
1216        let msg = dec.decode(&raw).unwrap();
1217
1218        let mut iter = msg.groups(&MISC_FEES);
1219        assert!(iter.next().is_none());
1220    }
1221
1222    // -----------------------------------------------------------------------
1223    // GroupIter — fields() and find() on a Group
1224    // -----------------------------------------------------------------------
1225
1226    #[test]
1227    fn group_fields_iter() {
1228        let raw = fix("136=1|137=9.99|138=GBP|139=3|");
1229        let mut dec = Decoder::new();
1230        let msg = dec.decode(&raw).unwrap();
1231
1232        let g = msg.groups(&MISC_FEES).next().unwrap();
1233        let tags: Vec<Tag> = g.fields().map(|f| f.tag).collect();
1234        assert_eq!(
1235            tags,
1236            vec![tag::MISC_FEE_AMT, tag::MISC_FEE_CURR, tag::MISC_FEE_TYPE]
1237        );
1238    }
1239
1240    #[test]
1241    fn group_find_present_and_absent() {
1242        let raw = fix("136=1|137=9.99|138=GBP|");
1243        let mut dec = Decoder::new();
1244        let msg = dec.decode(&raw).unwrap();
1245
1246        let g = msg.groups(&MISC_FEES).next().unwrap();
1247        assert!(g.find(tag::MISC_FEE_AMT).is_some());
1248        assert!(g.find(tag::MISC_FEE_TYPE).is_none()); // not present in this instance
1249    }
1250
1251    // -----------------------------------------------------------------------
1252    // GroupIter — size_hint
1253    // -----------------------------------------------------------------------
1254
1255    #[test]
1256    fn group_iter_size_hint() {
1257        let raw = fix("136=3|137=1.0|137=2.0|137=3.0|");
1258        let mut dec = Decoder::new();
1259        let msg = dec.decode(&raw).unwrap();
1260
1261        let mut iter = msg.groups(&MISC_FEES);
1262        assert_eq!(iter.size_hint(), (3, Some(3)));
1263        iter.next();
1264        assert_eq!(iter.size_hint(), (2, Some(2)));
1265        iter.next();
1266        assert_eq!(iter.size_hint(), (1, Some(1)));
1267        iter.next();
1268        assert_eq!(iter.size_hint(), (0, Some(0)));
1269    }
1270
1271    // -----------------------------------------------------------------------
1272    // NO_MD_ENTRIES — most common market data use case
1273    // -----------------------------------------------------------------------
1274
1275    #[test]
1276    fn md_entries_two_instances() {
1277        // MDReqID + NO_MD_ENTRIES=2 + 2 entries each with MDEntryType/MDEntryPx/MDEntrySize
1278        let raw = fix("262=REQ1|268=2|269=0|270=100.50|271=500|269=1|270=100.75|271=300|");
1279        let mut dec = Decoder::new();
1280        let msg = dec.decode(&raw).unwrap();
1281
1282        let entries: Vec<_> = msg.groups(&MD_ENTRIES).collect();
1283        assert_eq!(entries.len(), 2);
1284
1285        let bid = &entries[0];
1286        assert_eq!(bid.find(tag::MD_ENTRY_TYPE).unwrap().value, b"0");
1287        assert_eq!(bid.find(tag::MD_ENTRY_PX).unwrap().value, b"100.50");
1288        assert_eq!(bid.find(tag::MD_ENTRY_SIZE).unwrap().value, b"500");
1289
1290        let offer = &entries[1];
1291        assert_eq!(offer.find(tag::MD_ENTRY_TYPE).unwrap().value, b"1");
1292        assert_eq!(offer.find(tag::MD_ENTRY_PX).unwrap().value, b"100.75");
1293        assert_eq!(offer.find(tag::MD_ENTRY_SIZE).unwrap().value, b"300");
1294    }
1295
1296    // -----------------------------------------------------------------------
1297    // Multiple different groups in the same message
1298    // -----------------------------------------------------------------------
1299
1300    #[test]
1301    fn multiple_group_types_in_message() {
1302        // A message with both NO_MISC_FEES and NO_ROUTING_IDS
1303        let raw =
1304            fix("35=D|136=1|137=1.50|138=USD|139=4|215=2|216=1|217=ROUTE_A|216=2|217=ROUTE_B|");
1305        let mut dec = Decoder::new();
1306        let msg = dec.decode(&raw).unwrap();
1307
1308        // Check MISC_FEES group
1309        let fees: Vec<_> = msg.groups(&MISC_FEES).collect();
1310        assert_eq!(fees.len(), 1);
1311        assert_eq!(fees[0].find(tag::MISC_FEE_AMT).unwrap().value, b"1.50");
1312
1313        // Check ROUTING_IDS group
1314        let routes: Vec<_> = msg.groups(&ROUTING_IDS).collect();
1315        assert_eq!(routes.len(), 2);
1316        assert_eq!(routes[0].find(tag::ROUTING_TYPE).unwrap().value, b"1");
1317        assert_eq!(routes[0].find(tag::ROUTING_ID).unwrap().value, b"ROUTE_A");
1318        assert_eq!(routes[1].find(tag::ROUTING_TYPE).unwrap().value, b"2");
1319        assert_eq!(routes[1].find(tag::ROUTING_ID).unwrap().value, b"ROUTE_B");
1320    }
1321
1322    // -----------------------------------------------------------------------
1323    // Group is_empty / len
1324    // -----------------------------------------------------------------------
1325
1326    #[test]
1327    fn group_is_empty_false_for_non_empty() {
1328        let raw = fix("136=1|137=1.0|");
1329        let mut dec = Decoder::new();
1330        let msg = dec.decode(&raw).unwrap();
1331        let g = msg.groups(&MISC_FEES).next().unwrap();
1332        assert!(!g.is_empty());
1333        assert_eq!(g.len(), 1);
1334    }
1335
1336    // -----------------------------------------------------------------------
1337    // Nested groups — Group::groups()
1338    // -----------------------------------------------------------------------
1339
1340    #[test]
1341    fn nested_group_single_parent_single_child() {
1342        // SIDES=1, one side with NO_CONT_AMTS=1
1343        let raw = fix("552=1|54=1|37=ORD1|518=1|519=1|520=100.00|521=USD|");
1344        let mut dec = Decoder::new();
1345        let msg = dec.decode(&raw).unwrap();
1346
1347        let side = msg.groups(&SIDES).next().expect("expected one side");
1348        assert_eq!(side.find(tag::SIDE).unwrap().value, b"1");
1349        assert_eq!(side.find(tag::ORDER_ID).unwrap().value, b"ORD1");
1350
1351        let mut nested = side.groups(&CONT_AMTS);
1352        let ca = nested.next().expect("expected one CONT_AMTS instance");
1353        assert!(nested.next().is_none());
1354        assert_eq!(ca.find(tag::CONT_AMT_TYPE).unwrap().value, b"1");
1355        assert_eq!(ca.find(tag::CONT_AMT_VALUE).unwrap().value, b"100.00");
1356        assert_eq!(ca.find(tag::CONT_AMT_CURR).unwrap().value, b"USD");
1357    }
1358
1359    #[test]
1360    fn nested_group_single_parent_multiple_children() {
1361        // SIDES=1, one side with NO_CONT_AMTS=2
1362        let raw = fix("552=1|54=1|518=2|519=1|520=100.00|521=USD|519=2|520=50.00|521=EUR|");
1363        let mut dec = Decoder::new();
1364        let msg = dec.decode(&raw).unwrap();
1365
1366        let side = msg.groups(&SIDES).next().unwrap();
1367        let cont_amts: Vec<_> = side.groups(&CONT_AMTS).collect();
1368        assert_eq!(cont_amts.len(), 2);
1369        assert_eq!(cont_amts[0].find(tag::CONT_AMT_TYPE).unwrap().value, b"1");
1370        assert_eq!(
1371            cont_amts[0].find(tag::CONT_AMT_VALUE).unwrap().value,
1372            b"100.00"
1373        );
1374        assert_eq!(cont_amts[0].find(tag::CONT_AMT_CURR).unwrap().value, b"USD");
1375        assert_eq!(cont_amts[1].find(tag::CONT_AMT_TYPE).unwrap().value, b"2");
1376        assert_eq!(
1377            cont_amts[1].find(tag::CONT_AMT_VALUE).unwrap().value,
1378            b"50.00"
1379        );
1380        assert_eq!(cont_amts[1].find(tag::CONT_AMT_CURR).unwrap().value, b"EUR");
1381    }
1382
1383    #[test]
1384    fn nested_group_multiple_parents_each_with_children() {
1385        // SIDES=2: side1 has 1 CONT_AMT, side2 has 2 CONT_AMTs — critical boundary test
1386        let raw = fix("552=2|\
1387             54=1|518=1|519=1|520=100.00|521=USD|\
1388             54=2|518=2|519=1|520=5.00|521=EUR|519=2|520=3.00|521=GBP|");
1389        let mut dec = Decoder::new();
1390        let msg = dec.decode(&raw).unwrap();
1391
1392        let sides: Vec<_> = msg.groups(&SIDES).collect();
1393        assert_eq!(sides.len(), 2);
1394
1395        let side1_cas: Vec<_> = sides[0].groups(&CONT_AMTS).collect();
1396        assert_eq!(side1_cas.len(), 1);
1397        assert_eq!(
1398            side1_cas[0].find(tag::CONT_AMT_VALUE).unwrap().value,
1399            b"100.00"
1400        );
1401        assert_eq!(side1_cas[0].find(tag::CONT_AMT_CURR).unwrap().value, b"USD");
1402
1403        let side2_cas: Vec<_> = sides[1].groups(&CONT_AMTS).collect();
1404        assert_eq!(side2_cas.len(), 2);
1405        assert_eq!(
1406            side2_cas[0].find(tag::CONT_AMT_VALUE).unwrap().value,
1407            b"5.00"
1408        );
1409        assert_eq!(side2_cas[0].find(tag::CONT_AMT_CURR).unwrap().value, b"EUR");
1410        assert_eq!(
1411            side2_cas[1].find(tag::CONT_AMT_VALUE).unwrap().value,
1412            b"3.00"
1413        );
1414        assert_eq!(side2_cas[1].find(tag::CONT_AMT_CURR).unwrap().value, b"GBP");
1415    }
1416
1417    #[test]
1418    fn nested_group_two_different_nested_groups_in_same_parent() {
1419        // SIDES=1, one side with NO_CONT_AMTS=1 and NO_MISC_FEES=1
1420        let raw = fix("552=1|54=1|518=1|519=1|520=100.00|521=USD|136=1|137=10.00|138=EUR|139=1|");
1421        let mut dec = Decoder::new();
1422        let msg = dec.decode(&raw).unwrap();
1423
1424        let side = msg.groups(&SIDES).next().unwrap();
1425
1426        let cont_amts: Vec<_> = side.groups(&CONT_AMTS).collect();
1427        assert_eq!(cont_amts.len(), 1);
1428        assert_eq!(
1429            cont_amts[0].find(tag::CONT_AMT_VALUE).unwrap().value,
1430            b"100.00"
1431        );
1432        assert_eq!(cont_amts[0].find(tag::CONT_AMT_CURR).unwrap().value, b"USD");
1433
1434        let misc_fees: Vec<_> = side.groups(&MISC_FEES).collect();
1435        assert_eq!(misc_fees.len(), 1);
1436        assert_eq!(
1437            misc_fees[0].find(tag::MISC_FEE_AMT).unwrap().value,
1438            b"10.00"
1439        );
1440        assert_eq!(misc_fees[0].find(tag::MISC_FEE_CURR).unwrap().value, b"EUR");
1441        assert_eq!(misc_fees[0].find(tag::MISC_FEE_TYPE).unwrap().value, b"1");
1442    }
1443
1444    #[test]
1445    fn nested_group_count_tag_absent_yields_empty() {
1446        // SIDES=1, one side with no CONT_AMTS tag at all
1447        let raw = fix("552=1|54=1|37=ORD1|");
1448        let mut dec = Decoder::new();
1449        let msg = dec.decode(&raw).unwrap();
1450
1451        let side = msg.groups(&SIDES).next().unwrap();
1452        assert!(side.groups(&CONT_AMTS).next().is_none());
1453    }
1454
1455    #[test]
1456    fn nested_group_count_zero_yields_empty() {
1457        // SIDES=1, one side with NO_CONT_AMTS=0
1458        let raw = fix("552=1|54=1|37=ORD1|518=0|");
1459        let mut dec = Decoder::new();
1460        let msg = dec.decode(&raw).unwrap();
1461
1462        let side = msg.groups(&SIDES).next().unwrap();
1463        assert!(side.groups(&CONT_AMTS).next().is_none());
1464    }
1465
1466    #[test]
1467    fn nested_group_iter_size_hint() {
1468        // SIDES=1, one side with NO_CONT_AMTS=3
1469        let raw = fix("552=1|54=1|518=3|519=1|520=10.00|519=2|520=20.00|519=3|520=30.00|");
1470        let mut dec = Decoder::new();
1471        let msg = dec.decode(&raw).unwrap();
1472
1473        let side = msg.groups(&SIDES).next().unwrap();
1474        let mut nested = side.groups(&CONT_AMTS);
1475        assert_eq!(nested.size_hint(), (3, Some(3)));
1476        nested.next();
1477        assert_eq!(nested.size_hint(), (2, Some(2)));
1478        nested.next();
1479        assert_eq!(nested.size_hint(), (1, Some(1)));
1480        nested.next();
1481        assert_eq!(nested.size_hint(), (0, Some(0)));
1482        assert!(nested.next().is_none());
1483    }
1484}