Skip to main content

zerodds_ccm/
transform.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Equivalent-IDL transformation — Spec §6.3.2 / §6.4.1 / §6.5.1 /
5//! §6.6.x / §6.7.1.
6//!
7//! Input: `zerodds_idl::ast::ComponentDef` / `HomeDef` / `EventDef`.
8//! Output: one or more `InterfaceDef` (all spec-conformant) that
9//! represent the "implicitly defined equivalent interface" of the CCM
10//! spec.
11//!
12//! Spec §6.2 (S. 11): "A component definition in IDL implicitly defines
13//! an interface that supports the features defined in the component
14//! definition body."
15
16use alloc::format;
17use alloc::string::{String, ToString};
18use alloc::vec::Vec;
19
20use zerodds_idl::ast::{
21    AttrDecl, ComponentDef, ComponentExport, EventDef, Export, HomeDef, Identifier, InterfaceDef,
22    InterfaceKind, OpDecl, ParamAttribute, ParamDecl, PrimitiveType, ScopedName, StringType,
23    TypeSpec, ValueKind,
24};
25use zerodds_idl::errors::Span;
26
27/// Result of the component transformation: the equivalent interface
28/// (Spec §6.3.2) plus all implied event-consumer interfaces (Spec
29/// §6.6.6.2 "EventConsumers" module).
30#[derive(Debug, Clone, PartialEq)]
31pub struct ComponentEquivalent {
32    /// The component equivalent interface (Spec §6.3.2).
33    pub equivalent_interface: InterfaceDef,
34    /// Implied `<event_type>Consumer` interfaces for the
35    /// `consumes`/`emits`/`publishes` ports (Spec §6.6.6.2 "EventConsumers"
36    /// scope).
37    pub event_consumer_interfaces: Vec<InterfaceDef>,
38}
39
40/// Result of the home transformation: the three interfaces from Spec
41/// §6.7.1 (Explicit + Implicit + Equivalent).
42#[derive(Debug, Clone, PartialEq)]
43pub struct HomeEquivalent {
44    /// `<home_name>Explicit : Components::CCMHome [, supported]`.
45    pub explicit: InterfaceDef,
46    /// `<home_name>Implicit : Components::KeylessCCMHome` OR the
47    /// keyed variant without inheritance.
48    pub implicit: InterfaceDef,
49    /// `<home_name> : <home_name>Explicit, <home_name>Implicit { }`.
50    pub equivalent: InterfaceDef,
51}
52
53/// Result of the event-type transformation — Spec §6.6.1.1.
54///
55/// From `eventtype E { state-members }`, two IDL definitions are
56/// derived:
57/// 1. `valuetype E { state-members }` (with additional inheritance from
58///    `Components::EventBase` for the first eventtype in a chain).
59/// 2. `interface EConsumer : Components::EventConsumerBase { void
60///    push_E(in E the_e); };`.
61#[derive(Debug, Clone, PartialEq)]
62pub struct EventTypeEquivalent {
63    /// The equivalent valuetype name (with `: Components::EventBase` for
64    /// the first in the chain).
65    pub valuetype_name: Identifier,
66    /// Inheritance bases of the valuetype (per Spec §6.6.1.1: first in
67    /// the chain `Components::EventBase`, derived eventtype `BaseValueType,
68    /// :: Components::EventBase`).
69    pub valuetype_bases: Vec<ScopedName>,
70    /// `<event_type>Consumer` interface (Spec §6.6.1.1).
71    pub consumer_interface: InterfaceDef,
72}
73
74/// Spec §6.3.2 (p. 12) + §6.4.1 / §6.5.1 / §6.6.x — generates the
75/// component equivalent interface including all port operations.
76///
77/// **Inheritance (§6.3.2):**
78/// * Without `supports` and without `base`: `: Components::CCMObject`.
79/// * With `base`: `: <base>` (CCMObject is transitive).
80/// * With `supports`: additionally the supported interfaces.
81///
82/// **Body (§6.4.1 / §6.5.1 / §6.6.x):** all port decls are translated
83/// into operations on the equivalent interface.
84#[must_use]
85pub fn transform_component(comp: &ComponentDef) -> ComponentEquivalent {
86    let span = Span::SYNTHETIC;
87
88    // Inheritance: §6.3.2.
89    let bases = component_bases(comp, span);
90
91    let mut exports: Vec<Export> = Vec::new();
92    let mut event_consumer_ifaces: Vec<InterfaceDef> = Vec::new();
93    let mut emitted_consumers: Vec<String> = Vec::new();
94
95    for export in &comp.body {
96        match export {
97            ComponentExport::Provides {
98                type_spec, name, ..
99            } => {
100                exports.push(Export::Op(provide_facet_op(name, type_spec, span)));
101            }
102            ComponentExport::Uses {
103                type_spec,
104                name,
105                multiple,
106                ..
107            } => {
108                exports.extend(uses_ops(name, type_spec, *multiple, span));
109            }
110            ComponentExport::Attribute(attr) => {
111                exports.push(Export::Attr(attr_to_attr_decl(attr, span)));
112            }
113            ComponentExport::Emits {
114                type_spec, name, ..
115            } => {
116                ensure_consumer_interface(
117                    type_spec,
118                    span,
119                    &mut event_consumer_ifaces,
120                    &mut emitted_consumers,
121                );
122                exports.extend(emits_ops(name, type_spec, span));
123            }
124            ComponentExport::Publishes {
125                type_spec, name, ..
126            } => {
127                ensure_consumer_interface(
128                    type_spec,
129                    span,
130                    &mut event_consumer_ifaces,
131                    &mut emitted_consumers,
132                );
133                exports.extend(publishes_ops(name, type_spec, span));
134            }
135            ComponentExport::Consumes {
136                type_spec, name, ..
137            } => {
138                ensure_consumer_interface(
139                    type_spec,
140                    span,
141                    &mut event_consumer_ifaces,
142                    &mut emitted_consumers,
143                );
144                exports.push(Export::Op(consumes_op(name, type_spec, span)));
145            }
146            ComponentExport::Port { .. } => {
147                // Spec §7.4.11 (IDL4) — a `port` decl is a user-defined
148                // port type. The equivalent-IDL mapping is defined via the
149                // porttype (the spec refers to the connector mapping).
150                // In CCM 4.0 this is not directly §6 but rather §7.4.11
151                // IDL4 (add-on); here we do not invoke it and leave it to
152                // the caller if desired.
153            }
154        }
155    }
156
157    let equivalent_interface = InterfaceDef {
158        kind: InterfaceKind::Plain,
159        name: comp.name.clone(),
160        bases,
161        exports,
162        annotations: Vec::new(),
163        span,
164    };
165
166    ComponentEquivalent {
167        equivalent_interface,
168        event_consumer_interfaces: event_consumer_ifaces,
169    }
170}
171
172/// Spec §6.7.1 (p. 33) — generates the three interfaces (Explicit,
173/// Implicit, Equivalent) from a `home` decl.
174#[must_use]
175pub fn transform_home(home: &HomeDef) -> HomeEquivalent {
176    let span = Span::SYNTHETIC;
177    let h = &home.name.text;
178
179    // Spec §6.7.1.3 (p. 35): the Explicit iface inherits CCMHome + supported.
180    let mut explicit_bases = alloc::vec![scoped(&["Components", "CCMHome"], span)];
181    explicit_bases.extend(home.supports.iter().cloned());
182    if let Some(base) = &home.base {
183        // Spec §6.7.4 (p. 37) — derived home: `<home>Explicit :
184        // <base_home>Explicit`.
185        explicit_bases = alloc::vec![base_explicit_name(base, span)];
186        explicit_bases.extend(home.supports.iter().cloned());
187    }
188
189    let explicit = InterfaceDef {
190        kind: InterfaceKind::Plain,
191        name: Identifier::new(format!("{h}Explicit"), span),
192        bases: explicit_bases,
193        // Spec §6.7.3 — the Explicit iface contains the explicitly
194        // declared operations + attributes; factory/finder are converted
195        // (see `factory_op_to_explicit` / `finder_op_to_explicit`); here
196        // we return an empty list because the body is not further modeled
197        // in HomeDef (completing it is the caller's responsibility for
198        // factory/finder decls).
199        exports: Vec::new(),
200        annotations: Vec::new(),
201        span,
202    };
203
204    // Spec §6.7.1.1 (no primary key) vs §6.7.1.2 (with primary key).
205    let implicit = if let Some(pk) = &home.primary_key {
206        build_keyed_implicit(h, &home.manages, pk, span)
207    } else {
208        build_keyless_implicit(h, &home.manages, span)
209    };
210
211    // Spec §6.7.1.x — the Equivalent iface inherits Explicit + Implicit.
212    let equivalent = InterfaceDef {
213        kind: InterfaceKind::Plain,
214        name: home.name.clone(),
215        bases: alloc::vec![
216            ScopedName::single(Identifier::new(format!("{h}Explicit"), span)),
217            ScopedName::single(Identifier::new(format!("{h}Implicit"), span)),
218        ],
219        exports: Vec::new(),
220        annotations: Vec::new(),
221        span,
222    };
223
224    HomeEquivalent {
225        explicit,
226        implicit,
227        equivalent,
228    }
229}
230
231/// Spec §6.6.1.1 (p. 24) — eventtype → valuetype + consumer interface.
232#[must_use]
233pub fn transform_event_type(et: &EventDef) -> EventTypeEquivalent {
234    let span = Span::SYNTHETIC;
235
236    // Spec §6.6.1.1 (p. 25): "the first event type in the inheritance
237    // chain introduces the inheritance from Components::EventBase".
238    // If EventDef already has an inheritance, EventBase is NOT added
239    // again.
240    let mut bases: Vec<ScopedName> = Vec::new();
241    let mut has_inheritance = false;
242    if let Some(inherit) = &et.inheritance {
243        if !inherit.bases.is_empty() {
244            has_inheritance = true;
245            bases.extend(inherit.bases.iter().cloned());
246        }
247    }
248    if !has_inheritance {
249        // First eventtype in the chain → EventBase as inheritance.
250        bases.push(scoped(&["Components", "EventBase"], span));
251    }
252
253    // Consumer interface (Spec §6.6.1.1 p. 25).
254    let consumer_iface_name = format!("{}Consumer", et.name.text);
255    let push_op = OpDecl {
256        name: Identifier::new(format!("push_{}", et.name.text), span),
257        oneway: false,
258        context: Vec::new(),
259        return_type: None,
260        params: alloc::vec![ParamDecl {
261            attribute: ParamAttribute::In,
262            type_spec: TypeSpec::Scoped(ScopedName::single(et.name.clone())),
263            name: Identifier::new(format!("the_{}", lowercase_first(&et.name.text)), span),
264            annotations: Vec::new(),
265            span,
266        }],
267        raises: Vec::new(),
268        annotations: Vec::new(),
269        span,
270    };
271    let consumer_bases = if has_inheritance {
272        // Spec §6.6.1.1 (p. 25): "Consumer interfaces are in the same
273        // inheritance relation as the event types".
274        et.inheritance.as_ref().map_or_else(
275            || alloc::vec![scoped(&["Components", "EventConsumerBase"], span)],
276            |inh| {
277                inh.bases
278                    .iter()
279                    .map(|b| {
280                        let last = b
281                            .parts
282                            .last()
283                            .map_or("Base", |i| i.text.as_str())
284                            .to_string();
285                        ScopedName::single(Identifier::new(format!("{last}Consumer"), span))
286                    })
287                    .collect()
288            },
289        )
290    } else {
291        alloc::vec![scoped(&["Components", "EventConsumerBase"], span)]
292    };
293
294    let consumer_iface = InterfaceDef {
295        kind: InterfaceKind::Plain,
296        name: Identifier::new(consumer_iface_name, span),
297        bases: consumer_bases,
298        exports: alloc::vec![Export::Op(push_op)],
299        annotations: Vec::new(),
300        span,
301    };
302
303    let _ = ValueKind::Concrete; // Mark unused-import safe.
304
305    EventTypeEquivalent {
306        valuetype_name: et.name.clone(),
307        valuetype_bases: bases,
308        consumer_interface: consumer_iface,
309    }
310}
311
312// ============================================================================
313// Internal helpers — Spec-by-Spec.
314// ============================================================================
315
316fn component_bases(comp: &ComponentDef, span: Span) -> Vec<ScopedName> {
317    // Spec §6.3.2 — inheritance + supports.
318    let mut bases = Vec::new();
319    if let Some(b) = &comp.base {
320        bases.push(b.clone());
321    } else {
322        bases.push(scoped(&["Components", "CCMObject"], span));
323    }
324    bases.extend(comp.supports.iter().cloned());
325    bases
326}
327
328/// Spec §6.4.1 (p. 13) — `provides T name` → `T provide_name();`.
329fn provide_facet_op(name: &Identifier, iface_type: &ScopedName, span: Span) -> OpDecl {
330    OpDecl {
331        name: Identifier::new(format!("provide_{}", name.text), span),
332        oneway: false,
333        context: Vec::new(),
334        return_type: Some(TypeSpec::Scoped(iface_type.clone())),
335        params: Vec::new(),
336        raises: Vec::new(),
337        annotations: Vec::new(),
338        span,
339    }
340}
341
342/// Spec §6.5.1 (p. 19) — `uses [multiple] T name` → connect/disconnect/
343/// get_connection(s) operations.
344fn uses_ops(name: &Identifier, iface_type: &ScopedName, multiple: bool, span: Span) -> Vec<Export> {
345    let n = &name.text;
346    let mut out = Vec::new();
347    if multiple {
348        // Spec §6.5.1 (p. 19-20) — multiplex.
349        // Cookie connect_<name>(in <T> connection) raises (...);
350        out.push(Export::Op(OpDecl {
351            name: Identifier::new(format!("connect_{n}"), span),
352            oneway: false,
353            context: Vec::new(),
354            return_type: Some(TypeSpec::Scoped(scoped(&["Components", "Cookie"], span))),
355            params: alloc::vec![ParamDecl {
356                attribute: ParamAttribute::In,
357                type_spec: TypeSpec::Scoped(iface_type.clone()),
358                name: Identifier::new("connection", span),
359                annotations: Vec::new(),
360                span,
361            }],
362            raises: alloc::vec![
363                scoped(&["Components", "ExceededConnectionLimit"], span),
364                scoped(&["Components", "InvalidConnection"], span),
365            ],
366            annotations: Vec::new(),
367            span,
368        }));
369        // <T> disconnect_<name>(in Components::Cookie ck) raises (...);
370        out.push(Export::Op(OpDecl {
371            name: Identifier::new(format!("disconnect_{n}"), span),
372            oneway: false,
373            context: Vec::new(),
374            return_type: Some(TypeSpec::Scoped(iface_type.clone())),
375            params: alloc::vec![ParamDecl {
376                attribute: ParamAttribute::In,
377                type_spec: TypeSpec::Scoped(scoped(&["Components", "Cookie"], span)),
378                name: Identifier::new("ck", span),
379                annotations: Vec::new(),
380                span,
381            }],
382            raises: alloc::vec![scoped(&["Components", "InvalidConnection"], span)],
383            annotations: Vec::new(),
384            span,
385        }));
386        // <name>Connections get_connections_<name>();
387        out.push(Export::Op(OpDecl {
388            name: Identifier::new(format!("get_connections_{n}"), span),
389            oneway: false,
390            context: Vec::new(),
391            return_type: Some(TypeSpec::Scoped(ScopedName::single(Identifier::new(
392                format!("{n}Connections"),
393                span,
394            )))),
395            params: Vec::new(),
396            raises: Vec::new(),
397            annotations: Vec::new(),
398            span,
399        }));
400    } else {
401        // Spec §6.5.1 (p. 19) — simplex.
402        out.push(Export::Op(OpDecl {
403            name: Identifier::new(format!("connect_{n}"), span),
404            oneway: false,
405            context: Vec::new(),
406            return_type: None,
407            params: alloc::vec![ParamDecl {
408                attribute: ParamAttribute::In,
409                type_spec: TypeSpec::Scoped(iface_type.clone()),
410                name: Identifier::new("conxn", span),
411                annotations: Vec::new(),
412                span,
413            }],
414            raises: alloc::vec![
415                scoped(&["Components", "AlreadyConnected"], span),
416                scoped(&["Components", "InvalidConnection"], span),
417            ],
418            annotations: Vec::new(),
419            span,
420        }));
421        out.push(Export::Op(OpDecl {
422            name: Identifier::new(format!("disconnect_{n}"), span),
423            oneway: false,
424            context: Vec::new(),
425            return_type: Some(TypeSpec::Scoped(iface_type.clone())),
426            params: Vec::new(),
427            raises: alloc::vec![scoped(&["Components", "NoConnection"], span)],
428            annotations: Vec::new(),
429            span,
430        }));
431        out.push(Export::Op(OpDecl {
432            name: Identifier::new(format!("get_connection_{n}"), span),
433            oneway: false,
434            context: Vec::new(),
435            return_type: Some(TypeSpec::Scoped(iface_type.clone())),
436            params: Vec::new(),
437            raises: Vec::new(),
438            annotations: Vec::new(),
439            span,
440        }));
441    }
442    out
443}
444
445/// Spec §6.6.6.1 (p. 28) — `emits T name` → `void connect_<name>(in
446/// TConsumer)` + `TConsumer disconnect_<name>()`.
447fn emits_ops(name: &Identifier, event_type: &ScopedName, span: Span) -> Vec<Export> {
448    let n = &name.text;
449    let consumer_type = consumer_type_of(event_type, span);
450    alloc::vec![
451        Export::Op(OpDecl {
452            name: Identifier::new(format!("connect_{n}"), span),
453            oneway: false,
454            context: Vec::new(),
455            return_type: None,
456            params: alloc::vec![ParamDecl {
457                attribute: ParamAttribute::In,
458                type_spec: TypeSpec::Scoped(consumer_type.clone()),
459                name: Identifier::new("consumer", span),
460                annotations: Vec::new(),
461                span,
462            }],
463            raises: alloc::vec![scoped(&["Components", "AlreadyConnected"], span)],
464            annotations: Vec::new(),
465            span,
466        }),
467        Export::Op(OpDecl {
468            name: Identifier::new(format!("disconnect_{n}"), span),
469            oneway: false,
470            context: Vec::new(),
471            return_type: Some(TypeSpec::Scoped(consumer_type)),
472            params: Vec::new(),
473            raises: alloc::vec![scoped(&["Components", "NoConnection"], span)],
474            annotations: Vec::new(),
475            span,
476        }),
477    ]
478}
479
480/// Spec §6.6.5.1 (p. 27) — `publishes T name` → `Cookie subscribe_<name>
481/// (in TConsumer consumer)` + `TConsumer unsubscribe_<name>(in
482/// Cookie ck)`.
483fn publishes_ops(name: &Identifier, event_type: &ScopedName, span: Span) -> Vec<Export> {
484    let n = &name.text;
485    let consumer_type = consumer_type_of(event_type, span);
486    alloc::vec![
487        Export::Op(OpDecl {
488            name: Identifier::new(format!("subscribe_{n}"), span),
489            oneway: false,
490            context: Vec::new(),
491            return_type: Some(TypeSpec::Scoped(scoped(&["Components", "Cookie"], span))),
492            params: alloc::vec![ParamDecl {
493                attribute: ParamAttribute::In,
494                type_spec: TypeSpec::Scoped(consumer_type.clone()),
495                name: Identifier::new("consumer", span),
496                annotations: Vec::new(),
497                span,
498            }],
499            raises: alloc::vec![scoped(&["Components", "ExceededConnectionLimit"], span)],
500            annotations: Vec::new(),
501            span,
502        }),
503        Export::Op(OpDecl {
504            name: Identifier::new(format!("unsubscribe_{n}"), span),
505            oneway: false,
506            context: Vec::new(),
507            return_type: Some(TypeSpec::Scoped(consumer_type)),
508            params: alloc::vec![ParamDecl {
509                attribute: ParamAttribute::In,
510                type_spec: TypeSpec::Scoped(scoped(&["Components", "Cookie"], span)),
511                name: Identifier::new("ck", span),
512                annotations: Vec::new(),
513                span,
514            }],
515            raises: alloc::vec![scoped(&["Components", "InvalidConnection"], span)],
516            annotations: Vec::new(),
517            span,
518        }),
519    ]
520}
521
522/// Spec §6.6.7.1 (p. 29) — `consumes T name` → `TConsumer
523/// get_consumer_<name>();`.
524fn consumes_op(name: &Identifier, event_type: &ScopedName, span: Span) -> OpDecl {
525    let n = &name.text;
526    let consumer_type = consumer_type_of(event_type, span);
527    OpDecl {
528        name: Identifier::new(format!("get_consumer_{n}"), span),
529        oneway: false,
530        context: Vec::new(),
531        return_type: Some(TypeSpec::Scoped(consumer_type)),
532        params: Vec::new(),
533        raises: Vec::new(),
534        annotations: Vec::new(),
535        span,
536    }
537}
538
539fn ensure_consumer_interface(
540    event_type: &ScopedName,
541    span: Span,
542    out: &mut Vec<InterfaceDef>,
543    emitted: &mut Vec<String>,
544) {
545    let consumer_name = consumer_simple_name(event_type);
546    if emitted.iter().any(|n| n == &consumer_name) {
547        return;
548    }
549    emitted.push(consumer_name.clone());
550    let push_op = OpDecl {
551        name: Identifier::new(
552            format!(
553                "push_{}",
554                event_type.parts.last().map_or("E", |i| i.text.as_str())
555            ),
556            span,
557        ),
558        oneway: false,
559        context: Vec::new(),
560        return_type: None,
561        params: alloc::vec![ParamDecl {
562            attribute: ParamAttribute::In,
563            type_spec: TypeSpec::Scoped(event_type.clone()),
564            name: Identifier::new(
565                format!(
566                    "the_{}",
567                    lowercase_first(event_type.parts.last().map_or("e", |i| i.text.as_str()))
568                ),
569                span,
570            ),
571            annotations: Vec::new(),
572            span,
573        }],
574        raises: Vec::new(),
575        annotations: Vec::new(),
576        span,
577    };
578    out.push(InterfaceDef {
579        kind: InterfaceKind::Plain,
580        name: Identifier::new(consumer_name, span),
581        bases: alloc::vec![scoped(&["Components", "EventConsumerBase"], span)],
582        exports: alloc::vec![Export::Op(push_op)],
583        annotations: Vec::new(),
584        span,
585    });
586}
587
588fn consumer_type_of(event_type: &ScopedName, span: Span) -> ScopedName {
589    let mut parts = event_type.parts.clone();
590    if let Some(last) = parts.last_mut() {
591        last.text.push_str("Consumer");
592    } else {
593        parts.push(Identifier::new("EConsumer", span));
594    }
595    ScopedName {
596        absolute: event_type.absolute,
597        parts,
598        span,
599    }
600}
601
602fn consumer_simple_name(event_type: &ScopedName) -> String {
603    event_type.parts.last().map_or_else(
604        || String::from("EConsumer"),
605        |i| format!("{}Consumer", i.text),
606    )
607}
608
609/// Converts the `zerodds_idl::ast::AttrDcl` (CCM component attribute with
610/// 4 fields) into the full `zerodds_idl::ast::AttrDecl` with 8 fields.
611fn attr_to_attr_decl(attr: &zerodds_idl::ast::AttrDcl, span: Span) -> AttrDecl {
612    AttrDecl {
613        name: attr.name.clone(),
614        type_spec: attr.type_spec.clone(),
615        readonly: attr.readonly,
616        get_raises: Vec::new(),
617        set_raises: Vec::new(),
618        annotations: Vec::new(),
619        span,
620    }
621}
622
623fn build_keyless_implicit(
624    home_name: &str,
625    component_type: &ScopedName,
626    span: Span,
627) -> InterfaceDef {
628    // Spec §6.7.1.1 (p. 33) — `interface <h>Implicit :
629    // Components::KeylessCCMHome { <component> create() raises
630    // (CreateFailure); };`.
631    InterfaceDef {
632        kind: InterfaceKind::Plain,
633        name: Identifier::new(format!("{home_name}Implicit"), span),
634        bases: alloc::vec![scoped(&["Components", "KeylessCCMHome"], span)],
635        exports: alloc::vec![Export::Op(OpDecl {
636            name: Identifier::new("create", span),
637            oneway: false,
638            context: Vec::new(),
639            return_type: Some(TypeSpec::Scoped(component_type.clone())),
640            params: Vec::new(),
641            raises: alloc::vec![scoped(&["Components", "CreateFailure"], span)],
642            annotations: Vec::new(),
643            span,
644        })],
645        annotations: Vec::new(),
646        span,
647    }
648}
649
650fn build_keyed_implicit(
651    home_name: &str,
652    component_type: &ScopedName,
653    key_type: &ScopedName,
654    span: Span,
655) -> InterfaceDef {
656    // Spec §6.7.1.2 (p. 34): keyed `<h>Implicit` contains create/
657    // find_by_primary_key/remove/get_primary_key. No inheritance —
658    // the operations are the only body entries.
659    let exports = alloc::vec![
660        // <comp> create(in <K> key) raises (CreateFailure, DuplicateKeyValue, InvalidKey);
661        Export::Op(OpDecl {
662            name: Identifier::new("create", span),
663            oneway: false,
664            context: Vec::new(),
665            return_type: Some(TypeSpec::Scoped(component_type.clone())),
666            params: alloc::vec![ParamDecl {
667                attribute: ParamAttribute::In,
668                type_spec: TypeSpec::Scoped(key_type.clone()),
669                name: Identifier::new("key", span),
670                annotations: Vec::new(),
671                span,
672            }],
673            raises: alloc::vec![
674                scoped(&["Components", "CreateFailure"], span),
675                scoped(&["Components", "DuplicateKeyValue"], span),
676                scoped(&["Components", "InvalidKey"], span),
677            ],
678            annotations: Vec::new(),
679            span,
680        }),
681        // <comp> find_by_primary_key(in <K> key) raises (FinderFailure, UnknownKeyValue, InvalidKey);
682        Export::Op(OpDecl {
683            name: Identifier::new("find_by_primary_key", span),
684            oneway: false,
685            context: Vec::new(),
686            return_type: Some(TypeSpec::Scoped(component_type.clone())),
687            params: alloc::vec![ParamDecl {
688                attribute: ParamAttribute::In,
689                type_spec: TypeSpec::Scoped(key_type.clone()),
690                name: Identifier::new("key", span),
691                annotations: Vec::new(),
692                span,
693            }],
694            raises: alloc::vec![
695                scoped(&["Components", "FinderFailure"], span),
696                scoped(&["Components", "UnknownKeyValue"], span),
697                scoped(&["Components", "InvalidKey"], span),
698            ],
699            annotations: Vec::new(),
700            span,
701        }),
702        // void remove(in <K> key) raises (RemoveFailure, UnknownKeyValue, InvalidKey);
703        Export::Op(OpDecl {
704            name: Identifier::new("remove", span),
705            oneway: false,
706            context: Vec::new(),
707            return_type: None,
708            params: alloc::vec![ParamDecl {
709                attribute: ParamAttribute::In,
710                type_spec: TypeSpec::Scoped(key_type.clone()),
711                name: Identifier::new("key", span),
712                annotations: Vec::new(),
713                span,
714            }],
715            raises: alloc::vec![
716                scoped(&["Components", "RemoveFailure"], span),
717                scoped(&["Components", "UnknownKeyValue"], span),
718                scoped(&["Components", "InvalidKey"], span),
719            ],
720            annotations: Vec::new(),
721            span,
722        }),
723        // <K> get_primary_key(in <comp> comp);
724        Export::Op(OpDecl {
725            name: Identifier::new("get_primary_key", span),
726            oneway: false,
727            context: Vec::new(),
728            return_type: Some(TypeSpec::Scoped(key_type.clone())),
729            params: alloc::vec![ParamDecl {
730                attribute: ParamAttribute::In,
731                type_spec: TypeSpec::Scoped(component_type.clone()),
732                name: Identifier::new("comp", span),
733                annotations: Vec::new(),
734                span,
735            }],
736            raises: Vec::new(),
737            annotations: Vec::new(),
738            span,
739        }),
740    ];
741
742    InterfaceDef {
743        kind: InterfaceKind::Plain,
744        name: Identifier::new(format!("{home_name}Implicit"), span),
745        // Spec §6.7.1.2 (p. 34): no direct inheritance; operations are
746        // the complete members.
747        bases: Vec::new(),
748        exports,
749        annotations: Vec::new(),
750        span,
751    }
752}
753
754fn base_explicit_name(base: &ScopedName, span: Span) -> ScopedName {
755    let last_text = base
756        .parts
757        .last()
758        .map_or_else(|| String::from("BaseHome"), |i| i.text.clone());
759    ScopedName {
760        absolute: base.absolute,
761        parts: alloc::vec![Identifier::new(format!("{last_text}Explicit"), span)],
762        span,
763    }
764}
765
766fn scoped(parts: &[&str], span: Span) -> ScopedName {
767    ScopedName {
768        absolute: false,
769        parts: parts
770            .iter()
771            .map(|p| Identifier::new((*p).to_string(), span))
772            .collect(),
773        span,
774    }
775}
776
777/// Public re-export for `validate.rs` (Phase-B-Cluster-9).
778#[must_use]
779pub fn scoped_name(parts: &[&str], span: Span) -> ScopedName {
780    scoped(parts, span)
781}
782
783fn lowercase_first(s: &str) -> String {
784    let mut chars = s.chars();
785    chars.next().map_or_else(String::new, |c| {
786        let mut out: String = c.to_lowercase().collect();
787        out.push_str(chars.as_str());
788        out
789    })
790}
791
792// Mark unused-import safe (StringType, PrimitiveType used only in tests).
793const _: fn() = || {
794    let _ = core::marker::PhantomData::<StringType>;
795    let _ = core::marker::PhantomData::<PrimitiveType>;
796};
797
798#[cfg(test)]
799#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
800mod tests {
801    use super::*;
802    use zerodds_idl::ast::{ComponentDef, ComponentExport, IntegerType};
803
804    fn span() -> Span {
805        Span::SYNTHETIC
806    }
807
808    fn ident(s: &str) -> Identifier {
809        Identifier::new(s, span())
810    }
811
812    fn sn(parts: &[&str]) -> ScopedName {
813        scoped(parts, span())
814    }
815
816    fn comp(name: &str, body: Vec<ComponentExport>) -> ComponentDef {
817        ComponentDef {
818            name: ident(name),
819            base: None,
820            supports: Vec::new(),
821            body,
822            annotations: Vec::new(),
823            span: span(),
824        }
825    }
826
827    fn comp_with_supports(
828        name: &str,
829        supports: Vec<ScopedName>,
830        body: Vec<ComponentExport>,
831    ) -> ComponentDef {
832        ComponentDef {
833            name: ident(name),
834            base: None,
835            supports,
836            body,
837            annotations: Vec::new(),
838            span: span(),
839        }
840    }
841
842    fn comp_with_base(name: &str, base: ScopedName, body: Vec<ComponentExport>) -> ComponentDef {
843        ComponentDef {
844            name: ident(name),
845            base: Some(base),
846            supports: Vec::new(),
847            body,
848            annotations: Vec::new(),
849            span: span(),
850        }
851    }
852
853    fn op_names(iface: &InterfaceDef) -> Vec<String> {
854        iface
855            .exports
856            .iter()
857            .filter_map(|e| match e {
858                Export::Op(o) => Some(o.name.text.clone()),
859                _ => None,
860            })
861            .collect()
862    }
863
864    #[test]
865    fn simple_basic_component_inherits_ccmobject() {
866        // Spec §6.3.2.1 (p. 12) — `component C {};` →
867        // `interface C : Components::CCMObject {};`.
868        let c = comp("C", alloc::vec![]);
869        let out = transform_component(&c);
870        assert_eq!(out.equivalent_interface.name.text, "C");
871        assert_eq!(out.equivalent_interface.bases.len(), 1);
872        let parts: Vec<&str> = out.equivalent_interface.bases[0]
873            .parts
874            .iter()
875            .map(|p| p.text.as_str())
876            .collect();
877        assert_eq!(parts, alloc::vec!["Components", "CCMObject"]);
878    }
879
880    #[test]
881    fn component_with_supports_inherits_ccmobject_plus_supported() {
882        // Spec §6.3.2.2 (p. 12) — `component C supports I1, I2 { };` →
883        // `interface C : Components::CCMObject, I1, I2 {};`.
884        let c = comp_with_supports("C", alloc::vec![sn(&["I1"]), sn(&["I2"])], alloc::vec![]);
885        let out = transform_component(&c);
886        let names: Vec<String> = out
887            .equivalent_interface
888            .bases
889            .iter()
890            .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
891            .collect();
892        assert_eq!(names, alloc::vec!["CCMObject", "I1", "I2"]);
893    }
894
895    #[test]
896    fn component_with_base_inherits_base_not_ccmobject() {
897        // Spec §6.3.2.3 (p. 12) — `component C : B { };` →
898        // `interface C : B { ... }` (CCMObject is transitive via B).
899        let c = comp_with_base("C", sn(&["B"]), alloc::vec![]);
900        let out = transform_component(&c);
901        let parts: Vec<String> = out.equivalent_interface.bases[0]
902            .parts
903            .iter()
904            .map(|p| p.text.clone())
905            .collect();
906        assert_eq!(parts, alloc::vec!["B"]);
907    }
908
909    #[test]
910    fn provides_decl_yields_provide_underscore_name_op() {
911        // Spec §6.4.1 (p. 13) — `provides I foo;` → `I provide_foo();`.
912        let c = comp(
913            "C",
914            alloc::vec![ComponentExport::Provides {
915                type_spec: sn(&["M", "I"]),
916                name: ident("foo"),
917                span: span(),
918            }],
919        );
920        let out = transform_component(&c);
921        let names = op_names(&out.equivalent_interface);
922        assert!(names.contains(&String::from("provide_foo")));
923    }
924
925    #[test]
926    fn uses_simplex_yields_three_ops_with_correct_signatures() {
927        // Spec §6.5.1 (p. 19).
928        let c = comp(
929            "C",
930            alloc::vec![ComponentExport::Uses {
931                type_spec: sn(&["I"]),
932                name: ident("manager"),
933                multiple: false,
934                span: span(),
935            }],
936        );
937        let out = transform_component(&c);
938        let names = op_names(&out.equivalent_interface);
939        assert!(names.contains(&String::from("connect_manager")));
940        assert!(names.contains(&String::from("disconnect_manager")));
941        assert!(names.contains(&String::from("get_connection_manager")));
942    }
943
944    #[test]
945    fn uses_multiple_yields_get_connections_plural_op() {
946        // Spec §6.5.1 (p. 19-20) — multiplex receptacle.
947        let c = comp(
948            "C",
949            alloc::vec![ComponentExport::Uses {
950                type_spec: sn(&["I"]),
951                name: ident("managers"),
952                multiple: true,
953                span: span(),
954            }],
955        );
956        let out = transform_component(&c);
957        let names = op_names(&out.equivalent_interface);
958        assert!(names.contains(&String::from("connect_managers")));
959        assert!(names.contains(&String::from("disconnect_managers")));
960        assert!(names.contains(&String::from("get_connections_managers")));
961        // Connect returns a Cookie (Spec §6.5.1 p. 20 multiplex).
962        let connect = out
963            .equivalent_interface
964            .exports
965            .iter()
966            .find_map(|e| match e {
967                Export::Op(o) if o.name.text == "connect_managers" => Some(o),
968                _ => None,
969            })
970            .expect("connect");
971        let TypeSpec::Scoped(ret) = connect.return_type.as_ref().expect("return") else {
972            panic!()
973        };
974        assert_eq!(
975            ret.parts
976                .iter()
977                .map(|i| i.text.as_str())
978                .collect::<Vec<_>>(),
979            alloc::vec!["Components", "Cookie"]
980        );
981    }
982
983    #[test]
984    fn emits_decl_yields_connect_disconnect_with_consumer() {
985        // Spec §6.6.6.1 (p. 28).
986        let c = comp(
987            "C",
988            alloc::vec![ComponentExport::Emits {
989                type_spec: sn(&["Tick"]),
990                name: ident("ticker"),
991                span: span(),
992            }],
993        );
994        let out = transform_component(&c);
995        let names = op_names(&out.equivalent_interface);
996        assert!(names.contains(&String::from("connect_ticker")));
997        assert!(names.contains(&String::from("disconnect_ticker")));
998        // The TickConsumer iface was created implicitly.
999        assert_eq!(out.event_consumer_interfaces.len(), 1);
1000        assert_eq!(out.event_consumer_interfaces[0].name.text, "TickConsumer");
1001    }
1002
1003    #[test]
1004    fn publishes_decl_yields_subscribe_unsubscribe_with_cookie() {
1005        // Spec §6.6.5.1 (p. 27).
1006        let c = comp(
1007            "C",
1008            alloc::vec![ComponentExport::Publishes {
1009                type_spec: sn(&["Tick"]),
1010                name: ident("ticker"),
1011                span: span(),
1012            }],
1013        );
1014        let out = transform_component(&c);
1015        let names = op_names(&out.equivalent_interface);
1016        assert!(names.contains(&String::from("subscribe_ticker")));
1017        assert!(names.contains(&String::from("unsubscribe_ticker")));
1018        // subscribe returns a Cookie (Spec §6.6.5.1).
1019        let sub = out
1020            .equivalent_interface
1021            .exports
1022            .iter()
1023            .find_map(|e| match e {
1024                Export::Op(o) if o.name.text == "subscribe_ticker" => Some(o),
1025                _ => None,
1026            })
1027            .expect("subscribe");
1028        let TypeSpec::Scoped(ret) = sub.return_type.as_ref().expect("return") else {
1029            panic!()
1030        };
1031        assert_eq!(
1032            ret.parts
1033                .iter()
1034                .map(|i| i.text.as_str())
1035                .collect::<Vec<_>>(),
1036            alloc::vec!["Components", "Cookie"]
1037        );
1038    }
1039
1040    #[test]
1041    fn consumes_decl_yields_get_consumer_op() {
1042        // Spec §6.6.7.1 (p. 29) — `consumes Tick sink;` →
1043        // `TickConsumer get_consumer_sink();`.
1044        let c = comp(
1045            "C",
1046            alloc::vec![ComponentExport::Consumes {
1047                type_spec: sn(&["Tick"]),
1048                name: ident("sink"),
1049                span: span(),
1050            }],
1051        );
1052        let out = transform_component(&c);
1053        let names = op_names(&out.equivalent_interface);
1054        assert!(names.contains(&String::from("get_consumer_sink")));
1055        // Return type is TickConsumer.
1056        let op = out
1057            .equivalent_interface
1058            .exports
1059            .iter()
1060            .find_map(|e| match e {
1061                Export::Op(o) if o.name.text == "get_consumer_sink" => Some(o),
1062                _ => None,
1063            })
1064            .expect("op");
1065        let TypeSpec::Scoped(ret) = op.return_type.as_ref().expect("return") else {
1066            panic!()
1067        };
1068        assert_eq!(
1069            ret.parts.last().expect("last").text.as_str(),
1070            "TickConsumer"
1071        );
1072    }
1073
1074    #[test]
1075    fn duplicate_event_type_yields_only_one_consumer_interface() {
1076        // Spec §6.6 — one consumer iface per EventType, not per port.
1077        let c = comp(
1078            "C",
1079            alloc::vec![
1080                ComponentExport::Emits {
1081                    type_spec: sn(&["Tick"]),
1082                    name: ident("a"),
1083                    span: span(),
1084                },
1085                ComponentExport::Publishes {
1086                    type_spec: sn(&["Tick"]),
1087                    name: ident("b"),
1088                    span: span(),
1089                },
1090                ComponentExport::Consumes {
1091                    type_spec: sn(&["Tick"]),
1092                    name: ident("c"),
1093                    span: span(),
1094                },
1095            ],
1096        );
1097        let out = transform_component(&c);
1098        assert_eq!(out.event_consumer_interfaces.len(), 1);
1099    }
1100
1101    #[test]
1102    fn attribute_is_propagated_to_equivalent_interface() {
1103        let c = comp(
1104            "C",
1105            alloc::vec![ComponentExport::Attribute(zerodds_idl::ast::AttrDcl {
1106                readonly: false,
1107                type_spec: TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long)),
1108                name: ident("rate"),
1109                span: span(),
1110            })],
1111        );
1112        let out = transform_component(&c);
1113        let attr = out
1114            .equivalent_interface
1115            .exports
1116            .iter()
1117            .find_map(|e| match e {
1118                Export::Attr(a) => Some(a),
1119                _ => None,
1120            })
1121            .expect("attr");
1122        assert_eq!(attr.name.text, "rate");
1123        assert!(!attr.readonly);
1124    }
1125
1126    #[test]
1127    fn home_without_primary_key_yields_keyless_implicit() {
1128        // Spec §6.7.1.1 (p. 33).
1129        let h = HomeDef {
1130            name: ident("CManager"),
1131            base: None,
1132            supports: Vec::new(),
1133            manages: sn(&["C"]),
1134            primary_key: None,
1135            annotations: Vec::new(),
1136            span: span(),
1137        };
1138        let out = transform_home(&h);
1139        assert_eq!(out.explicit.name.text, "CManagerExplicit");
1140        assert_eq!(out.implicit.name.text, "CManagerImplicit");
1141        assert_eq!(out.equivalent.name.text, "CManager");
1142        // Explicit inherits CCMHome.
1143        let parts = out.explicit.bases[0]
1144            .parts
1145            .iter()
1146            .map(|i| i.text.as_str())
1147            .collect::<Vec<_>>();
1148        assert_eq!(parts, alloc::vec!["Components", "CCMHome"]);
1149        // Implicit inherits KeylessCCMHome.
1150        let parts = out.implicit.bases[0]
1151            .parts
1152            .iter()
1153            .map(|i| i.text.as_str())
1154            .collect::<Vec<_>>();
1155        assert_eq!(parts, alloc::vec!["Components", "KeylessCCMHome"]);
1156        // Implicit has a create() op.
1157        let names = op_names(&out.implicit);
1158        assert_eq!(names, alloc::vec!["create"]);
1159    }
1160
1161    #[test]
1162    fn home_with_primary_key_yields_keyed_implicit_with_four_ops() {
1163        // Spec §6.7.1.2 (p. 34).
1164        let h = HomeDef {
1165            name: ident("CManager"),
1166            base: None,
1167            supports: Vec::new(),
1168            manages: sn(&["C"]),
1169            primary_key: Some(sn(&["CKey"])),
1170            annotations: Vec::new(),
1171            span: span(),
1172        };
1173        let out = transform_home(&h);
1174        let names = op_names(&out.implicit);
1175        for expected in ["create", "find_by_primary_key", "remove", "get_primary_key"] {
1176            assert!(
1177                names.contains(&String::from(expected)),
1178                "missing {expected} in {names:?}"
1179            );
1180        }
1181        // Keyed-Implicit has NO inheritance (only body ops).
1182        assert!(out.implicit.bases.is_empty());
1183    }
1184
1185    #[test]
1186    fn derived_home_inherits_base_explicit() {
1187        // Spec §6.7.4 (p. 37): "Each Explicit Home, derived from another
1188        // home, MUST inherit from the parent's Explicit Interface".
1189        // dds-ts-1.0-beta1 cross-ref: omg-ccm-4.0 §6.7.4.
1190        let h = HomeDef {
1191            name: ident("CManagerExt"),
1192            base: Some(sn(&["CManagerBase"])),
1193            supports: Vec::new(),
1194            manages: sn(&["C"]),
1195            primary_key: None,
1196            annotations: Vec::new(),
1197            span: span(),
1198        };
1199        let out = transform_home(&h);
1200        // The Explicit iface of CManagerExt inherits from
1201        // CManagerBaseExplicit, NOT from Components::CCMHome (the
1202        // inheritance root is already anchored in the base Explicit).
1203        assert_eq!(out.explicit.name.text, "CManagerExtExplicit");
1204        assert_eq!(
1205            out.explicit.bases.len(),
1206            1,
1207            "derived home explicit must inherit exactly the base's Explicit"
1208        );
1209        let parts = out.explicit.bases[0]
1210            .parts
1211            .iter()
1212            .map(|i| i.text.as_str())
1213            .collect::<Vec<_>>();
1214        assert_eq!(
1215            parts,
1216            alloc::vec!["CManagerBaseExplicit"],
1217            "expected CManagerBaseExplicit, got {parts:?}"
1218        );
1219    }
1220
1221    #[test]
1222    fn derived_home_with_supports_extends_base_explicit() {
1223        // Spec §6.7.4: derived home MAY add supports clauses on top
1224        // of the base-explicit inheritance.
1225        let h = HomeDef {
1226            name: ident("CManagerExt"),
1227            base: Some(sn(&["CManagerBase"])),
1228            supports: alloc::vec![sn(&["IExtraIface"])],
1229            manages: sn(&["C"]),
1230            primary_key: None,
1231            annotations: Vec::new(),
1232            span: span(),
1233        };
1234        let out = transform_home(&h);
1235        let names: Vec<String> = out
1236            .explicit
1237            .bases
1238            .iter()
1239            .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
1240            .collect();
1241        assert_eq!(
1242            names,
1243            alloc::vec![
1244                String::from("CManagerBaseExplicit"),
1245                String::from("IExtraIface")
1246            ]
1247        );
1248    }
1249
1250    #[test]
1251    fn equivalent_home_inherits_explicit_and_implicit() {
1252        let h = HomeDef {
1253            name: ident("CManager"),
1254            base: None,
1255            supports: Vec::new(),
1256            manages: sn(&["C"]),
1257            primary_key: None,
1258            annotations: Vec::new(),
1259            span: span(),
1260        };
1261        let out = transform_home(&h);
1262        let names: Vec<String> = out
1263            .equivalent
1264            .bases
1265            .iter()
1266            .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
1267            .collect();
1268        assert_eq!(names, alloc::vec!["CManagerExplicit", "CManagerImplicit"]);
1269    }
1270
1271    #[test]
1272    fn event_type_first_in_chain_inherits_event_base() {
1273        // Spec §6.6.1.1 (p. 25).
1274        let et = EventDef {
1275            name: ident("Tick"),
1276            kind: ValueKind::Concrete,
1277            inheritance: None,
1278            elements: Vec::new(),
1279            annotations: Vec::new(),
1280            span: span(),
1281        };
1282        let out = transform_event_type(&et);
1283        // valuetype Tick : Components::EventBase.
1284        let parts = out.valuetype_bases[0]
1285            .parts
1286            .iter()
1287            .map(|i| i.text.as_str())
1288            .collect::<Vec<_>>();
1289        assert_eq!(parts, alloc::vec!["Components", "EventBase"]);
1290        // TickConsumer : Components::EventConsumerBase.
1291        assert_eq!(out.consumer_interface.name.text, "TickConsumer");
1292        let cb = out.consumer_interface.bases[0]
1293            .parts
1294            .iter()
1295            .map(|i| i.text.as_str())
1296            .collect::<Vec<_>>();
1297        assert_eq!(cb, alloc::vec!["Components", "EventConsumerBase"]);
1298        // push_Tick(in Tick the_tick).
1299        let push = out.consumer_interface.exports.iter().find_map(|e| match e {
1300            Export::Op(o) => Some(o),
1301            _ => None,
1302        });
1303        let push = push.expect("push op");
1304        assert_eq!(push.name.text, "push_Tick");
1305        assert_eq!(push.params[0].name.text, "the_tick");
1306    }
1307
1308    #[test]
1309    fn full_stockmanager_component_yields_all_expected_ops() {
1310        // End-to-end: component with all port kinds.
1311        let c = comp(
1312            "Trader",
1313            alloc::vec![
1314                ComponentExport::Provides {
1315                    type_spec: sn(&["StockManager"]),
1316                    name: ident("manager"),
1317                    span: span(),
1318                },
1319                ComponentExport::Uses {
1320                    type_spec: sn(&["Bank"]),
1321                    name: ident("bank"),
1322                    multiple: false,
1323                    span: span(),
1324                },
1325                ComponentExport::Uses {
1326                    type_spec: sn(&["Feed"]),
1327                    name: ident("feeds"),
1328                    multiple: true,
1329                    span: span(),
1330                },
1331                ComponentExport::Emits {
1332                    type_spec: sn(&["Tick"]),
1333                    name: ident("ticker"),
1334                    span: span(),
1335                },
1336                ComponentExport::Publishes {
1337                    type_spec: sn(&["Tick"]),
1338                    name: ident("public_ticker"),
1339                    span: span(),
1340                },
1341                ComponentExport::Consumes {
1342                    type_spec: sn(&["Order"]),
1343                    name: ident("order_sink"),
1344                    span: span(),
1345                },
1346            ],
1347        );
1348        let out = transform_component(&c);
1349        let names = op_names(&out.equivalent_interface);
1350        for expected in [
1351            "provide_manager",
1352            "connect_bank",
1353            "disconnect_bank",
1354            "get_connection_bank",
1355            "connect_feeds",
1356            "disconnect_feeds",
1357            "get_connections_feeds",
1358            "connect_ticker",
1359            "disconnect_ticker",
1360            "subscribe_public_ticker",
1361            "unsubscribe_public_ticker",
1362            "get_consumer_order_sink",
1363        ] {
1364            assert!(
1365                names.contains(&String::from(expected)),
1366                "missing {expected} in {names:?}"
1367            );
1368        }
1369        // Tick + Order Consumers (2).
1370        let consumer_names: Vec<String> = out
1371            .event_consumer_interfaces
1372            .iter()
1373            .map(|i| i.name.text.clone())
1374            .collect();
1375        assert!(consumer_names.contains(&String::from("TickConsumer")));
1376        assert!(consumer_names.contains(&String::from("OrderConsumer")));
1377    }
1378}