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//! Eingabe: `zerodds_idl::ast::ComponentDef` / `HomeDef` / `EventDef`.
8//! Ausgabe: ein oder mehrere `InterfaceDef` (alle Spec-konform), die
9//! die "implicitly defined equivalent interface" der CCM-Spec
10//! darstellen.
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/// Ergebnis der Component-Transformation: das Equivalent-Interface
28/// (Spec §6.3.2) plus alle implied Event-Consumer-Interfaces (Spec
29/// §6.6.6.2 "EventConsumers"-Module).
30#[derive(Debug, Clone, PartialEq)]
31pub struct ComponentEquivalent {
32    /// Das Component-Equivalent-Interface (Spec §6.3.2).
33    pub equivalent_interface: InterfaceDef,
34    /// Implied `<event_type>Consumer` Interfaces fuer die
35    /// `consumes`/`emits`/`publishes`-Ports (Spec §6.6.6.2 "EventConsumers"
36    /// scope).
37    pub event_consumer_interfaces: Vec<InterfaceDef>,
38}
39
40/// Ergebnis der Home-Transformation: die drei Interfaces aus 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` ODER
47    /// keyed-Variante ohne Inheritance.
48    pub implicit: InterfaceDef,
49    /// `<home_name> : <home_name>Explicit, <home_name>Implicit { }`.
50    pub equivalent: InterfaceDef,
51}
52
53/// Ergebnis der EventType-Transformation — Spec §6.6.1.1.
54///
55/// Aus `eventtype E { state-members }` werden zwei IDL-Definitionen
56/// abgeleitet:
57/// 1. `valuetype E { state-members }` (mit zusaetzlicher Vererbung von
58///    `Components::EventBase` fuer die erste eventtype in einer Kette).
59/// 2. `interface EConsumer : Components::EventConsumerBase { void
60///    push_E(in E the_e); };`.
61#[derive(Debug, Clone, PartialEq)]
62pub struct EventTypeEquivalent {
63    /// Der equivalent valuetype-Name (mit `: Components::EventBase` fuer
64    /// erste in der Kette).
65    pub valuetype_name: Identifier,
66    /// Inheritance-Bases des Valuetype (nach Spec §6.6.1.1: erste in
67    /// Kette `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 (S. 12) + §6.4.1 / §6.5.1 / §6.6.x — generiert das
75/// Component-Equivalent-Interface inkl. aller Port-Operationen.
76///
77/// **Inheritance (§6.3.2):**
78/// * Ohne `supports` und ohne `base`: `: Components::CCMObject`.
79/// * Mit `base`: `: <base>` (CCMObject ist transitiv).
80/// * Mit `supports`: zusaetzlich die supported interfaces.
81///
82/// **Body (§6.4.1 / §6.5.1 / §6.6.x):** alle Port-Decls werden in
83/// Operationen auf dem Equivalent-Interface uebersetzt.
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) — `port`-Decl ist ein User-Defined
148                // Port-Type. Equivalent-IDL-Mapping ist ueber den
149                // porttype definiert (Spec verweist auf Connector-
150                // Mapping). Das ist in CCM 4.0 nicht direkt §6 sondern
151                // §7.4.11 IDL4 (Add-on); hier rufen wir das nicht auf
152                // und lassen das dem Caller, sofern gewollt.
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 (S. 33) — generiert die drei Interfaces (Explicit,
173/// Implicit, Equivalent) aus einem `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 (S. 35): 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 (S. 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 — Explicit-Iface enthaelt die explizit deklarierten
194        // Operations + Attributes; Factory/Finder werden umgewandelt
195        // (siehe `factory_op_to_explicit` / `finder_op_to_explicit`),
196        // hier liefern wir leere Liste, da der Body in HomeDef nicht
197        // weiter ausmodelliert ist (vollstaendig ist es Aufgabe des
198        // Callers fuer 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 — 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 (S. 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 (S. 25): "the first event type in the inheritance
237    // chain introduces the inheritance from Components::EventBase".
238    // Falls EventDef bereits eine Inheritance hat, kommt EventBase NICHT
239    // erneut dazu.
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        // Erste eventtype in Kette → EventBase als Vererbung.
250        bases.push(scoped(&["Components", "EventBase"], span));
251    }
252
253    // Consumer-Interface (Spec §6.6.1.1 S. 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        return_type: None,
259        params: alloc::vec![ParamDecl {
260            attribute: ParamAttribute::In,
261            type_spec: TypeSpec::Scoped(ScopedName::single(et.name.clone())),
262            name: Identifier::new(format!("the_{}", lowercase_first(&et.name.text)), span),
263            annotations: Vec::new(),
264            span,
265        }],
266        raises: Vec::new(),
267        annotations: Vec::new(),
268        span,
269    };
270    let consumer_bases = if has_inheritance {
271        // Spec §6.6.1.1 (S. 25): "Consumer interfaces are in the same
272        // inheritance relation as the event types".
273        et.inheritance.as_ref().map_or_else(
274            || alloc::vec![scoped(&["Components", "EventConsumerBase"], span)],
275            |inh| {
276                inh.bases
277                    .iter()
278                    .map(|b| {
279                        let last = b
280                            .parts
281                            .last()
282                            .map_or("Base", |i| i.text.as_str())
283                            .to_string();
284                        ScopedName::single(Identifier::new(format!("{last}Consumer"), span))
285                    })
286                    .collect()
287            },
288        )
289    } else {
290        alloc::vec![scoped(&["Components", "EventConsumerBase"], span)]
291    };
292
293    let consumer_iface = InterfaceDef {
294        kind: InterfaceKind::Plain,
295        name: Identifier::new(consumer_iface_name, span),
296        bases: consumer_bases,
297        exports: alloc::vec![Export::Op(push_op)],
298        annotations: Vec::new(),
299        span,
300    };
301
302    let _ = ValueKind::Concrete; // Mark unused-import safe.
303
304    EventTypeEquivalent {
305        valuetype_name: et.name.clone(),
306        valuetype_bases: bases,
307        consumer_interface: consumer_iface,
308    }
309}
310
311// ============================================================================
312// Internal helpers — Spec-by-Spec.
313// ============================================================================
314
315fn component_bases(comp: &ComponentDef, span: Span) -> Vec<ScopedName> {
316    // Spec §6.3.2 — Inheritance + supports.
317    let mut bases = Vec::new();
318    if let Some(b) = &comp.base {
319        bases.push(b.clone());
320    } else {
321        bases.push(scoped(&["Components", "CCMObject"], span));
322    }
323    bases.extend(comp.supports.iter().cloned());
324    bases
325}
326
327/// Spec §6.4.1 (S. 13) — `provides T name` → `T provide_name();`.
328fn provide_facet_op(name: &Identifier, iface_type: &ScopedName, span: Span) -> OpDecl {
329    OpDecl {
330        name: Identifier::new(format!("provide_{}", name.text), span),
331        oneway: false,
332        return_type: Some(TypeSpec::Scoped(iface_type.clone())),
333        params: Vec::new(),
334        raises: Vec::new(),
335        annotations: Vec::new(),
336        span,
337    }
338}
339
340/// Spec §6.5.1 (S. 19) — `uses [multiple] T name` → connect/disconnect/
341/// get_connection(s) Operations.
342fn uses_ops(name: &Identifier, iface_type: &ScopedName, multiple: bool, span: Span) -> Vec<Export> {
343    let n = &name.text;
344    let mut out = Vec::new();
345    if multiple {
346        // Spec §6.5.1 (S. 19-20) — multiplex.
347        // Cookie connect_<name>(in <T> connection) raises (...);
348        out.push(Export::Op(OpDecl {
349            name: Identifier::new(format!("connect_{n}"), span),
350            oneway: false,
351            return_type: Some(TypeSpec::Scoped(scoped(&["Components", "Cookie"], span))),
352            params: alloc::vec![ParamDecl {
353                attribute: ParamAttribute::In,
354                type_spec: TypeSpec::Scoped(iface_type.clone()),
355                name: Identifier::new("connection", span),
356                annotations: Vec::new(),
357                span,
358            }],
359            raises: alloc::vec![
360                scoped(&["Components", "ExceededConnectionLimit"], span),
361                scoped(&["Components", "InvalidConnection"], span),
362            ],
363            annotations: Vec::new(),
364            span,
365        }));
366        // <T> disconnect_<name>(in Components::Cookie ck) raises (...);
367        out.push(Export::Op(OpDecl {
368            name: Identifier::new(format!("disconnect_{n}"), span),
369            oneway: false,
370            return_type: Some(TypeSpec::Scoped(iface_type.clone())),
371            params: alloc::vec![ParamDecl {
372                attribute: ParamAttribute::In,
373                type_spec: TypeSpec::Scoped(scoped(&["Components", "Cookie"], span)),
374                name: Identifier::new("ck", span),
375                annotations: Vec::new(),
376                span,
377            }],
378            raises: alloc::vec![scoped(&["Components", "InvalidConnection"], span)],
379            annotations: Vec::new(),
380            span,
381        }));
382        // <name>Connections get_connections_<name>();
383        out.push(Export::Op(OpDecl {
384            name: Identifier::new(format!("get_connections_{n}"), span),
385            oneway: false,
386            return_type: Some(TypeSpec::Scoped(ScopedName::single(Identifier::new(
387                format!("{n}Connections"),
388                span,
389            )))),
390            params: Vec::new(),
391            raises: Vec::new(),
392            annotations: Vec::new(),
393            span,
394        }));
395    } else {
396        // Spec §6.5.1 (S. 19) — simplex.
397        out.push(Export::Op(OpDecl {
398            name: Identifier::new(format!("connect_{n}"), span),
399            oneway: false,
400            return_type: None,
401            params: alloc::vec![ParamDecl {
402                attribute: ParamAttribute::In,
403                type_spec: TypeSpec::Scoped(iface_type.clone()),
404                name: Identifier::new("conxn", span),
405                annotations: Vec::new(),
406                span,
407            }],
408            raises: alloc::vec![
409                scoped(&["Components", "AlreadyConnected"], span),
410                scoped(&["Components", "InvalidConnection"], span),
411            ],
412            annotations: Vec::new(),
413            span,
414        }));
415        out.push(Export::Op(OpDecl {
416            name: Identifier::new(format!("disconnect_{n}"), span),
417            oneway: false,
418            return_type: Some(TypeSpec::Scoped(iface_type.clone())),
419            params: Vec::new(),
420            raises: alloc::vec![scoped(&["Components", "NoConnection"], span)],
421            annotations: Vec::new(),
422            span,
423        }));
424        out.push(Export::Op(OpDecl {
425            name: Identifier::new(format!("get_connection_{n}"), span),
426            oneway: false,
427            return_type: Some(TypeSpec::Scoped(iface_type.clone())),
428            params: Vec::new(),
429            raises: Vec::new(),
430            annotations: Vec::new(),
431            span,
432        }));
433    }
434    out
435}
436
437/// Spec §6.6.6.1 (S. 28) — `emits T name` → `void connect_<name>(in
438/// TConsumer)` + `TConsumer disconnect_<name>()`.
439fn emits_ops(name: &Identifier, event_type: &ScopedName, span: Span) -> Vec<Export> {
440    let n = &name.text;
441    let consumer_type = consumer_type_of(event_type, span);
442    alloc::vec![
443        Export::Op(OpDecl {
444            name: Identifier::new(format!("connect_{n}"), span),
445            oneway: false,
446            return_type: None,
447            params: alloc::vec![ParamDecl {
448                attribute: ParamAttribute::In,
449                type_spec: TypeSpec::Scoped(consumer_type.clone()),
450                name: Identifier::new("consumer", span),
451                annotations: Vec::new(),
452                span,
453            }],
454            raises: alloc::vec![scoped(&["Components", "AlreadyConnected"], span)],
455            annotations: Vec::new(),
456            span,
457        }),
458        Export::Op(OpDecl {
459            name: Identifier::new(format!("disconnect_{n}"), span),
460            oneway: false,
461            return_type: Some(TypeSpec::Scoped(consumer_type)),
462            params: Vec::new(),
463            raises: alloc::vec![scoped(&["Components", "NoConnection"], span)],
464            annotations: Vec::new(),
465            span,
466        }),
467    ]
468}
469
470/// Spec §6.6.5.1 (S. 27) — `publishes T name` → `Cookie subscribe_<name>
471/// (in TConsumer consumer)` + `TConsumer unsubscribe_<name>(in
472/// Cookie ck)`.
473fn publishes_ops(name: &Identifier, event_type: &ScopedName, span: Span) -> Vec<Export> {
474    let n = &name.text;
475    let consumer_type = consumer_type_of(event_type, span);
476    alloc::vec![
477        Export::Op(OpDecl {
478            name: Identifier::new(format!("subscribe_{n}"), span),
479            oneway: false,
480            return_type: Some(TypeSpec::Scoped(scoped(&["Components", "Cookie"], span))),
481            params: alloc::vec![ParamDecl {
482                attribute: ParamAttribute::In,
483                type_spec: TypeSpec::Scoped(consumer_type.clone()),
484                name: Identifier::new("consumer", span),
485                annotations: Vec::new(),
486                span,
487            }],
488            raises: alloc::vec![scoped(&["Components", "ExceededConnectionLimit"], span)],
489            annotations: Vec::new(),
490            span,
491        }),
492        Export::Op(OpDecl {
493            name: Identifier::new(format!("unsubscribe_{n}"), span),
494            oneway: false,
495            return_type: Some(TypeSpec::Scoped(consumer_type)),
496            params: alloc::vec![ParamDecl {
497                attribute: ParamAttribute::In,
498                type_spec: TypeSpec::Scoped(scoped(&["Components", "Cookie"], span)),
499                name: Identifier::new("ck", span),
500                annotations: Vec::new(),
501                span,
502            }],
503            raises: alloc::vec![scoped(&["Components", "InvalidConnection"], span)],
504            annotations: Vec::new(),
505            span,
506        }),
507    ]
508}
509
510/// Spec §6.6.7.1 (S. 29) — `consumes T name` → `TConsumer
511/// get_consumer_<name>();`.
512fn consumes_op(name: &Identifier, event_type: &ScopedName, span: Span) -> OpDecl {
513    let n = &name.text;
514    let consumer_type = consumer_type_of(event_type, span);
515    OpDecl {
516        name: Identifier::new(format!("get_consumer_{n}"), span),
517        oneway: false,
518        return_type: Some(TypeSpec::Scoped(consumer_type)),
519        params: Vec::new(),
520        raises: Vec::new(),
521        annotations: Vec::new(),
522        span,
523    }
524}
525
526fn ensure_consumer_interface(
527    event_type: &ScopedName,
528    span: Span,
529    out: &mut Vec<InterfaceDef>,
530    emitted: &mut Vec<String>,
531) {
532    let consumer_name = consumer_simple_name(event_type);
533    if emitted.iter().any(|n| n == &consumer_name) {
534        return;
535    }
536    emitted.push(consumer_name.clone());
537    let push_op = OpDecl {
538        name: Identifier::new(
539            format!(
540                "push_{}",
541                event_type.parts.last().map_or("E", |i| i.text.as_str())
542            ),
543            span,
544        ),
545        oneway: false,
546        return_type: None,
547        params: alloc::vec![ParamDecl {
548            attribute: ParamAttribute::In,
549            type_spec: TypeSpec::Scoped(event_type.clone()),
550            name: Identifier::new(
551                format!(
552                    "the_{}",
553                    lowercase_first(event_type.parts.last().map_or("e", |i| i.text.as_str()))
554                ),
555                span,
556            ),
557            annotations: Vec::new(),
558            span,
559        }],
560        raises: Vec::new(),
561        annotations: Vec::new(),
562        span,
563    };
564    out.push(InterfaceDef {
565        kind: InterfaceKind::Plain,
566        name: Identifier::new(consumer_name, span),
567        bases: alloc::vec![scoped(&["Components", "EventConsumerBase"], span)],
568        exports: alloc::vec![Export::Op(push_op)],
569        annotations: Vec::new(),
570        span,
571    });
572}
573
574fn consumer_type_of(event_type: &ScopedName, span: Span) -> ScopedName {
575    let mut parts = event_type.parts.clone();
576    if let Some(last) = parts.last_mut() {
577        last.text.push_str("Consumer");
578    } else {
579        parts.push(Identifier::new("EConsumer", span));
580    }
581    ScopedName {
582        absolute: event_type.absolute,
583        parts,
584        span,
585    }
586}
587
588fn consumer_simple_name(event_type: &ScopedName) -> String {
589    event_type.parts.last().map_or_else(
590        || String::from("EConsumer"),
591        |i| format!("{}Consumer", i.text),
592    )
593}
594
595/// Konvertiert das `zerodds_idl::ast::AttrDcl` (CCM-Component-Attribute mit
596/// 4 Feldern) in das volle `zerodds_idl::ast::AttrDecl` mit 8 Feldern.
597fn attr_to_attr_decl(attr: &zerodds_idl::ast::AttrDcl, span: Span) -> AttrDecl {
598    AttrDecl {
599        name: attr.name.clone(),
600        type_spec: attr.type_spec.clone(),
601        readonly: attr.readonly,
602        get_raises: Vec::new(),
603        set_raises: Vec::new(),
604        annotations: Vec::new(),
605        span,
606    }
607}
608
609fn build_keyless_implicit(
610    home_name: &str,
611    component_type: &ScopedName,
612    span: Span,
613) -> InterfaceDef {
614    // Spec §6.7.1.1 (S. 33) — `interface <h>Implicit :
615    // Components::KeylessCCMHome { <component> create() raises
616    // (CreateFailure); };`.
617    InterfaceDef {
618        kind: InterfaceKind::Plain,
619        name: Identifier::new(format!("{home_name}Implicit"), span),
620        bases: alloc::vec![scoped(&["Components", "KeylessCCMHome"], span)],
621        exports: alloc::vec![Export::Op(OpDecl {
622            name: Identifier::new("create", span),
623            oneway: false,
624            return_type: Some(TypeSpec::Scoped(component_type.clone())),
625            params: Vec::new(),
626            raises: alloc::vec![scoped(&["Components", "CreateFailure"], span)],
627            annotations: Vec::new(),
628            span,
629        })],
630        annotations: Vec::new(),
631        span,
632    }
633}
634
635fn build_keyed_implicit(
636    home_name: &str,
637    component_type: &ScopedName,
638    key_type: &ScopedName,
639    span: Span,
640) -> InterfaceDef {
641    // Spec §6.7.1.2 (S. 34): keyed `<h>Implicit` enthaelt create/
642    // find_by_primary_key/remove/get_primary_key. Kein Inheritance —
643    // die Operationen sind die einzigen Body-Eintraege.
644    let exports = alloc::vec![
645        // <comp> create(in <K> key) raises (CreateFailure, DuplicateKeyValue, InvalidKey);
646        Export::Op(OpDecl {
647            name: Identifier::new("create", span),
648            oneway: false,
649            return_type: Some(TypeSpec::Scoped(component_type.clone())),
650            params: alloc::vec![ParamDecl {
651                attribute: ParamAttribute::In,
652                type_spec: TypeSpec::Scoped(key_type.clone()),
653                name: Identifier::new("key", span),
654                annotations: Vec::new(),
655                span,
656            }],
657            raises: alloc::vec![
658                scoped(&["Components", "CreateFailure"], span),
659                scoped(&["Components", "DuplicateKeyValue"], span),
660                scoped(&["Components", "InvalidKey"], span),
661            ],
662            annotations: Vec::new(),
663            span,
664        }),
665        // <comp> find_by_primary_key(in <K> key) raises (FinderFailure, UnknownKeyValue, InvalidKey);
666        Export::Op(OpDecl {
667            name: Identifier::new("find_by_primary_key", span),
668            oneway: false,
669            return_type: Some(TypeSpec::Scoped(component_type.clone())),
670            params: alloc::vec![ParamDecl {
671                attribute: ParamAttribute::In,
672                type_spec: TypeSpec::Scoped(key_type.clone()),
673                name: Identifier::new("key", span),
674                annotations: Vec::new(),
675                span,
676            }],
677            raises: alloc::vec![
678                scoped(&["Components", "FinderFailure"], span),
679                scoped(&["Components", "UnknownKeyValue"], span),
680                scoped(&["Components", "InvalidKey"], span),
681            ],
682            annotations: Vec::new(),
683            span,
684        }),
685        // void remove(in <K> key) raises (RemoveFailure, UnknownKeyValue, InvalidKey);
686        Export::Op(OpDecl {
687            name: Identifier::new("remove", span),
688            oneway: false,
689            return_type: None,
690            params: alloc::vec![ParamDecl {
691                attribute: ParamAttribute::In,
692                type_spec: TypeSpec::Scoped(key_type.clone()),
693                name: Identifier::new("key", span),
694                annotations: Vec::new(),
695                span,
696            }],
697            raises: alloc::vec![
698                scoped(&["Components", "RemoveFailure"], span),
699                scoped(&["Components", "UnknownKeyValue"], span),
700                scoped(&["Components", "InvalidKey"], span),
701            ],
702            annotations: Vec::new(),
703            span,
704        }),
705        // <K> get_primary_key(in <comp> comp);
706        Export::Op(OpDecl {
707            name: Identifier::new("get_primary_key", span),
708            oneway: false,
709            return_type: Some(TypeSpec::Scoped(key_type.clone())),
710            params: alloc::vec![ParamDecl {
711                attribute: ParamAttribute::In,
712                type_spec: TypeSpec::Scoped(component_type.clone()),
713                name: Identifier::new("comp", span),
714                annotations: Vec::new(),
715                span,
716            }],
717            raises: Vec::new(),
718            annotations: Vec::new(),
719            span,
720        }),
721    ];
722
723    InterfaceDef {
724        kind: InterfaceKind::Plain,
725        name: Identifier::new(format!("{home_name}Implicit"), span),
726        // Spec §6.7.1.2 (S. 34): keine direkte Vererbung; Operations sind
727        // die kompletten Members.
728        bases: Vec::new(),
729        exports,
730        annotations: Vec::new(),
731        span,
732    }
733}
734
735fn base_explicit_name(base: &ScopedName, span: Span) -> ScopedName {
736    let last_text = base
737        .parts
738        .last()
739        .map_or_else(|| String::from("BaseHome"), |i| i.text.clone());
740    ScopedName {
741        absolute: base.absolute,
742        parts: alloc::vec![Identifier::new(format!("{last_text}Explicit"), span)],
743        span,
744    }
745}
746
747fn scoped(parts: &[&str], span: Span) -> ScopedName {
748    ScopedName {
749        absolute: false,
750        parts: parts
751            .iter()
752            .map(|p| Identifier::new((*p).to_string(), span))
753            .collect(),
754        span,
755    }
756}
757
758/// Pub re-export fuer `validate.rs` (Phase-B-Cluster-9).
759#[must_use]
760pub fn scoped_name(parts: &[&str], span: Span) -> ScopedName {
761    scoped(parts, span)
762}
763
764fn lowercase_first(s: &str) -> String {
765    let mut chars = s.chars();
766    chars.next().map_or_else(String::new, |c| {
767        let mut out: String = c.to_lowercase().collect();
768        out.push_str(chars.as_str());
769        out
770    })
771}
772
773// Mark unused-import safe (StringType, PrimitiveType used only in tests).
774const _: fn() = || {
775    let _ = core::marker::PhantomData::<StringType>;
776    let _ = core::marker::PhantomData::<PrimitiveType>;
777};
778
779#[cfg(test)]
780#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
781mod tests {
782    use super::*;
783    use zerodds_idl::ast::{ComponentDef, ComponentExport, IntegerType};
784
785    fn span() -> Span {
786        Span::SYNTHETIC
787    }
788
789    fn ident(s: &str) -> Identifier {
790        Identifier::new(s, span())
791    }
792
793    fn sn(parts: &[&str]) -> ScopedName {
794        scoped(parts, span())
795    }
796
797    fn comp(name: &str, body: Vec<ComponentExport>) -> ComponentDef {
798        ComponentDef {
799            name: ident(name),
800            base: None,
801            supports: Vec::new(),
802            body,
803            annotations: Vec::new(),
804            span: span(),
805        }
806    }
807
808    fn comp_with_supports(
809        name: &str,
810        supports: Vec<ScopedName>,
811        body: Vec<ComponentExport>,
812    ) -> ComponentDef {
813        ComponentDef {
814            name: ident(name),
815            base: None,
816            supports,
817            body,
818            annotations: Vec::new(),
819            span: span(),
820        }
821    }
822
823    fn comp_with_base(name: &str, base: ScopedName, body: Vec<ComponentExport>) -> ComponentDef {
824        ComponentDef {
825            name: ident(name),
826            base: Some(base),
827            supports: Vec::new(),
828            body,
829            annotations: Vec::new(),
830            span: span(),
831        }
832    }
833
834    fn op_names(iface: &InterfaceDef) -> Vec<String> {
835        iface
836            .exports
837            .iter()
838            .filter_map(|e| match e {
839                Export::Op(o) => Some(o.name.text.clone()),
840                _ => None,
841            })
842            .collect()
843    }
844
845    #[test]
846    fn simple_basic_component_inherits_ccmobject() {
847        // Spec §6.3.2.1 (S. 12) — `component C {};` →
848        // `interface C : Components::CCMObject {};`.
849        let c = comp("C", alloc::vec![]);
850        let out = transform_component(&c);
851        assert_eq!(out.equivalent_interface.name.text, "C");
852        assert_eq!(out.equivalent_interface.bases.len(), 1);
853        let parts: Vec<&str> = out.equivalent_interface.bases[0]
854            .parts
855            .iter()
856            .map(|p| p.text.as_str())
857            .collect();
858        assert_eq!(parts, alloc::vec!["Components", "CCMObject"]);
859    }
860
861    #[test]
862    fn component_with_supports_inherits_ccmobject_plus_supported() {
863        // Spec §6.3.2.2 (S. 12) — `component C supports I1, I2 { };` →
864        // `interface C : Components::CCMObject, I1, I2 {};`.
865        let c = comp_with_supports("C", alloc::vec![sn(&["I1"]), sn(&["I2"])], alloc::vec![]);
866        let out = transform_component(&c);
867        let names: Vec<String> = out
868            .equivalent_interface
869            .bases
870            .iter()
871            .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
872            .collect();
873        assert_eq!(names, alloc::vec!["CCMObject", "I1", "I2"]);
874    }
875
876    #[test]
877    fn component_with_base_inherits_base_not_ccmobject() {
878        // Spec §6.3.2.3 (S. 12) — `component C : B { };` →
879        // `interface C : B { ... }` (CCMObject ist transitiv via B).
880        let c = comp_with_base("C", sn(&["B"]), alloc::vec![]);
881        let out = transform_component(&c);
882        let parts: Vec<String> = out.equivalent_interface.bases[0]
883            .parts
884            .iter()
885            .map(|p| p.text.clone())
886            .collect();
887        assert_eq!(parts, alloc::vec!["B"]);
888    }
889
890    #[test]
891    fn provides_decl_yields_provide_underscore_name_op() {
892        // Spec §6.4.1 (S. 13) — `provides I foo;` → `I provide_foo();`.
893        let c = comp(
894            "C",
895            alloc::vec![ComponentExport::Provides {
896                type_spec: sn(&["M", "I"]),
897                name: ident("foo"),
898                span: span(),
899            }],
900        );
901        let out = transform_component(&c);
902        let names = op_names(&out.equivalent_interface);
903        assert!(names.contains(&String::from("provide_foo")));
904    }
905
906    #[test]
907    fn uses_simplex_yields_three_ops_with_correct_signatures() {
908        // Spec §6.5.1 (S. 19).
909        let c = comp(
910            "C",
911            alloc::vec![ComponentExport::Uses {
912                type_spec: sn(&["I"]),
913                name: ident("manager"),
914                multiple: false,
915                span: span(),
916            }],
917        );
918        let out = transform_component(&c);
919        let names = op_names(&out.equivalent_interface);
920        assert!(names.contains(&String::from("connect_manager")));
921        assert!(names.contains(&String::from("disconnect_manager")));
922        assert!(names.contains(&String::from("get_connection_manager")));
923    }
924
925    #[test]
926    fn uses_multiple_yields_get_connections_plural_op() {
927        // Spec §6.5.1 (S. 19-20) — multiplex receptacle.
928        let c = comp(
929            "C",
930            alloc::vec![ComponentExport::Uses {
931                type_spec: sn(&["I"]),
932                name: ident("managers"),
933                multiple: true,
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_managers")));
940        assert!(names.contains(&String::from("disconnect_managers")));
941        assert!(names.contains(&String::from("get_connections_managers")));
942        // Connect liefert Cookie zurueck (Spec §6.5.1 S. 20 multiplex).
943        let connect = out
944            .equivalent_interface
945            .exports
946            .iter()
947            .find_map(|e| match e {
948                Export::Op(o) if o.name.text == "connect_managers" => Some(o),
949                _ => None,
950            })
951            .expect("connect");
952        let TypeSpec::Scoped(ret) = connect.return_type.as_ref().expect("return") else {
953            panic!()
954        };
955        assert_eq!(
956            ret.parts
957                .iter()
958                .map(|i| i.text.as_str())
959                .collect::<Vec<_>>(),
960            alloc::vec!["Components", "Cookie"]
961        );
962    }
963
964    #[test]
965    fn emits_decl_yields_connect_disconnect_with_consumer() {
966        // Spec §6.6.6.1 (S. 28).
967        let c = comp(
968            "C",
969            alloc::vec![ComponentExport::Emits {
970                type_spec: sn(&["Tick"]),
971                name: ident("ticker"),
972                span: span(),
973            }],
974        );
975        let out = transform_component(&c);
976        let names = op_names(&out.equivalent_interface);
977        assert!(names.contains(&String::from("connect_ticker")));
978        assert!(names.contains(&String::from("disconnect_ticker")));
979        // TickConsumer-Iface wurde implizit angelegt.
980        assert_eq!(out.event_consumer_interfaces.len(), 1);
981        assert_eq!(out.event_consumer_interfaces[0].name.text, "TickConsumer");
982    }
983
984    #[test]
985    fn publishes_decl_yields_subscribe_unsubscribe_with_cookie() {
986        // Spec §6.6.5.1 (S. 27).
987        let c = comp(
988            "C",
989            alloc::vec![ComponentExport::Publishes {
990                type_spec: sn(&["Tick"]),
991                name: ident("ticker"),
992                span: span(),
993            }],
994        );
995        let out = transform_component(&c);
996        let names = op_names(&out.equivalent_interface);
997        assert!(names.contains(&String::from("subscribe_ticker")));
998        assert!(names.contains(&String::from("unsubscribe_ticker")));
999        // subscribe liefert Cookie (Spec §6.6.5.1).
1000        let sub = out
1001            .equivalent_interface
1002            .exports
1003            .iter()
1004            .find_map(|e| match e {
1005                Export::Op(o) if o.name.text == "subscribe_ticker" => Some(o),
1006                _ => None,
1007            })
1008            .expect("subscribe");
1009        let TypeSpec::Scoped(ret) = sub.return_type.as_ref().expect("return") else {
1010            panic!()
1011        };
1012        assert_eq!(
1013            ret.parts
1014                .iter()
1015                .map(|i| i.text.as_str())
1016                .collect::<Vec<_>>(),
1017            alloc::vec!["Components", "Cookie"]
1018        );
1019    }
1020
1021    #[test]
1022    fn consumes_decl_yields_get_consumer_op() {
1023        // Spec §6.6.7.1 (S. 29) — `consumes Tick sink;` →
1024        // `TickConsumer get_consumer_sink();`.
1025        let c = comp(
1026            "C",
1027            alloc::vec![ComponentExport::Consumes {
1028                type_spec: sn(&["Tick"]),
1029                name: ident("sink"),
1030                span: span(),
1031            }],
1032        );
1033        let out = transform_component(&c);
1034        let names = op_names(&out.equivalent_interface);
1035        assert!(names.contains(&String::from("get_consumer_sink")));
1036        // Return-Typ ist TickConsumer.
1037        let op = out
1038            .equivalent_interface
1039            .exports
1040            .iter()
1041            .find_map(|e| match e {
1042                Export::Op(o) if o.name.text == "get_consumer_sink" => Some(o),
1043                _ => None,
1044            })
1045            .expect("op");
1046        let TypeSpec::Scoped(ret) = op.return_type.as_ref().expect("return") else {
1047            panic!()
1048        };
1049        assert_eq!(
1050            ret.parts.last().expect("last").text.as_str(),
1051            "TickConsumer"
1052        );
1053    }
1054
1055    #[test]
1056    fn duplicate_event_type_yields_only_one_consumer_interface() {
1057        // Spec §6.6 — Consumer-Iface pro EventType, nicht pro Port.
1058        let c = comp(
1059            "C",
1060            alloc::vec![
1061                ComponentExport::Emits {
1062                    type_spec: sn(&["Tick"]),
1063                    name: ident("a"),
1064                    span: span(),
1065                },
1066                ComponentExport::Publishes {
1067                    type_spec: sn(&["Tick"]),
1068                    name: ident("b"),
1069                    span: span(),
1070                },
1071                ComponentExport::Consumes {
1072                    type_spec: sn(&["Tick"]),
1073                    name: ident("c"),
1074                    span: span(),
1075                },
1076            ],
1077        );
1078        let out = transform_component(&c);
1079        assert_eq!(out.event_consumer_interfaces.len(), 1);
1080    }
1081
1082    #[test]
1083    fn attribute_is_propagated_to_equivalent_interface() {
1084        let c = comp(
1085            "C",
1086            alloc::vec![ComponentExport::Attribute(zerodds_idl::ast::AttrDcl {
1087                readonly: false,
1088                type_spec: TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long)),
1089                name: ident("rate"),
1090                span: span(),
1091            })],
1092        );
1093        let out = transform_component(&c);
1094        let attr = out
1095            .equivalent_interface
1096            .exports
1097            .iter()
1098            .find_map(|e| match e {
1099                Export::Attr(a) => Some(a),
1100                _ => None,
1101            })
1102            .expect("attr");
1103        assert_eq!(attr.name.text, "rate");
1104        assert!(!attr.readonly);
1105    }
1106
1107    #[test]
1108    fn home_without_primary_key_yields_keyless_implicit() {
1109        // Spec §6.7.1.1 (S. 33).
1110        let h = HomeDef {
1111            name: ident("CManager"),
1112            base: None,
1113            supports: Vec::new(),
1114            manages: sn(&["C"]),
1115            primary_key: None,
1116            annotations: Vec::new(),
1117            span: span(),
1118        };
1119        let out = transform_home(&h);
1120        assert_eq!(out.explicit.name.text, "CManagerExplicit");
1121        assert_eq!(out.implicit.name.text, "CManagerImplicit");
1122        assert_eq!(out.equivalent.name.text, "CManager");
1123        // Explicit inherits CCMHome.
1124        let parts = out.explicit.bases[0]
1125            .parts
1126            .iter()
1127            .map(|i| i.text.as_str())
1128            .collect::<Vec<_>>();
1129        assert_eq!(parts, alloc::vec!["Components", "CCMHome"]);
1130        // Implicit inherits KeylessCCMHome.
1131        let parts = out.implicit.bases[0]
1132            .parts
1133            .iter()
1134            .map(|i| i.text.as_str())
1135            .collect::<Vec<_>>();
1136        assert_eq!(parts, alloc::vec!["Components", "KeylessCCMHome"]);
1137        // Implicit hat eine create()-Op.
1138        let names = op_names(&out.implicit);
1139        assert_eq!(names, alloc::vec!["create"]);
1140    }
1141
1142    #[test]
1143    fn home_with_primary_key_yields_keyed_implicit_with_four_ops() {
1144        // Spec §6.7.1.2 (S. 34).
1145        let h = HomeDef {
1146            name: ident("CManager"),
1147            base: None,
1148            supports: Vec::new(),
1149            manages: sn(&["C"]),
1150            primary_key: Some(sn(&["CKey"])),
1151            annotations: Vec::new(),
1152            span: span(),
1153        };
1154        let out = transform_home(&h);
1155        let names = op_names(&out.implicit);
1156        for expected in ["create", "find_by_primary_key", "remove", "get_primary_key"] {
1157            assert!(
1158                names.contains(&String::from(expected)),
1159                "missing {expected} in {names:?}"
1160            );
1161        }
1162        // Keyed-Implicit hat KEINE Inheritance (nur Body-Ops).
1163        assert!(out.implicit.bases.is_empty());
1164    }
1165
1166    #[test]
1167    fn derived_home_inherits_base_explicit() {
1168        // Spec §6.7.4 (S. 37): "Each Explicit Home, derived from another
1169        // home, MUST inherit from the parent's Explicit Interface".
1170        // dds-ts-1.0-beta1 cross-ref: omg-ccm-4.0 §6.7.4.
1171        let h = HomeDef {
1172            name: ident("CManagerExt"),
1173            base: Some(sn(&["CManagerBase"])),
1174            supports: Vec::new(),
1175            manages: sn(&["C"]),
1176            primary_key: None,
1177            annotations: Vec::new(),
1178            span: span(),
1179        };
1180        let out = transform_home(&h);
1181        // Explicit-Iface von CManagerExt erbt von CManagerBaseExplicit,
1182        // NICHT von Components::CCMHome (die Inheritance-Wurzel ist
1183        // bereits im Base-Explicit verankert).
1184        assert_eq!(out.explicit.name.text, "CManagerExtExplicit");
1185        assert_eq!(
1186            out.explicit.bases.len(),
1187            1,
1188            "derived home explicit must inherit exactly the base's Explicit"
1189        );
1190        let parts = out.explicit.bases[0]
1191            .parts
1192            .iter()
1193            .map(|i| i.text.as_str())
1194            .collect::<Vec<_>>();
1195        assert_eq!(
1196            parts,
1197            alloc::vec!["CManagerBaseExplicit"],
1198            "expected CManagerBaseExplicit, got {parts:?}"
1199        );
1200    }
1201
1202    #[test]
1203    fn derived_home_with_supports_extends_base_explicit() {
1204        // Spec §6.7.4: derived home MAY add supports clauses on top
1205        // of the base-explicit inheritance.
1206        let h = HomeDef {
1207            name: ident("CManagerExt"),
1208            base: Some(sn(&["CManagerBase"])),
1209            supports: alloc::vec![sn(&["IExtraIface"])],
1210            manages: sn(&["C"]),
1211            primary_key: None,
1212            annotations: Vec::new(),
1213            span: span(),
1214        };
1215        let out = transform_home(&h);
1216        let names: Vec<String> = out
1217            .explicit
1218            .bases
1219            .iter()
1220            .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
1221            .collect();
1222        assert_eq!(
1223            names,
1224            alloc::vec![
1225                String::from("CManagerBaseExplicit"),
1226                String::from("IExtraIface")
1227            ]
1228        );
1229    }
1230
1231    #[test]
1232    fn equivalent_home_inherits_explicit_and_implicit() {
1233        let h = HomeDef {
1234            name: ident("CManager"),
1235            base: None,
1236            supports: Vec::new(),
1237            manages: sn(&["C"]),
1238            primary_key: None,
1239            annotations: Vec::new(),
1240            span: span(),
1241        };
1242        let out = transform_home(&h);
1243        let names: Vec<String> = out
1244            .equivalent
1245            .bases
1246            .iter()
1247            .map(|b| b.parts.last().map_or(String::new(), |i| i.text.clone()))
1248            .collect();
1249        assert_eq!(names, alloc::vec!["CManagerExplicit", "CManagerImplicit"]);
1250    }
1251
1252    #[test]
1253    fn event_type_first_in_chain_inherits_event_base() {
1254        // Spec §6.6.1.1 (S. 25).
1255        let et = EventDef {
1256            name: ident("Tick"),
1257            kind: ValueKind::Concrete,
1258            inheritance: None,
1259            elements: Vec::new(),
1260            annotations: Vec::new(),
1261            span: span(),
1262        };
1263        let out = transform_event_type(&et);
1264        // valuetype Tick : Components::EventBase.
1265        let parts = out.valuetype_bases[0]
1266            .parts
1267            .iter()
1268            .map(|i| i.text.as_str())
1269            .collect::<Vec<_>>();
1270        assert_eq!(parts, alloc::vec!["Components", "EventBase"]);
1271        // TickConsumer : Components::EventConsumerBase.
1272        assert_eq!(out.consumer_interface.name.text, "TickConsumer");
1273        let cb = out.consumer_interface.bases[0]
1274            .parts
1275            .iter()
1276            .map(|i| i.text.as_str())
1277            .collect::<Vec<_>>();
1278        assert_eq!(cb, alloc::vec!["Components", "EventConsumerBase"]);
1279        // push_Tick(in Tick the_tick).
1280        let push = out.consumer_interface.exports.iter().find_map(|e| match e {
1281            Export::Op(o) => Some(o),
1282            _ => None,
1283        });
1284        let push = push.expect("push op");
1285        assert_eq!(push.name.text, "push_Tick");
1286        assert_eq!(push.params[0].name.text, "the_tick");
1287    }
1288
1289    #[test]
1290    fn full_stockmanager_component_yields_all_expected_ops() {
1291        // End-to-End: Component mit allen Port-Arten.
1292        let c = comp(
1293            "Trader",
1294            alloc::vec![
1295                ComponentExport::Provides {
1296                    type_spec: sn(&["StockManager"]),
1297                    name: ident("manager"),
1298                    span: span(),
1299                },
1300                ComponentExport::Uses {
1301                    type_spec: sn(&["Bank"]),
1302                    name: ident("bank"),
1303                    multiple: false,
1304                    span: span(),
1305                },
1306                ComponentExport::Uses {
1307                    type_spec: sn(&["Feed"]),
1308                    name: ident("feeds"),
1309                    multiple: true,
1310                    span: span(),
1311                },
1312                ComponentExport::Emits {
1313                    type_spec: sn(&["Tick"]),
1314                    name: ident("ticker"),
1315                    span: span(),
1316                },
1317                ComponentExport::Publishes {
1318                    type_spec: sn(&["Tick"]),
1319                    name: ident("public_ticker"),
1320                    span: span(),
1321                },
1322                ComponentExport::Consumes {
1323                    type_spec: sn(&["Order"]),
1324                    name: ident("order_sink"),
1325                    span: span(),
1326                },
1327            ],
1328        );
1329        let out = transform_component(&c);
1330        let names = op_names(&out.equivalent_interface);
1331        for expected in [
1332            "provide_manager",
1333            "connect_bank",
1334            "disconnect_bank",
1335            "get_connection_bank",
1336            "connect_feeds",
1337            "disconnect_feeds",
1338            "get_connections_feeds",
1339            "connect_ticker",
1340            "disconnect_ticker",
1341            "subscribe_public_ticker",
1342            "unsubscribe_public_ticker",
1343            "get_consumer_order_sink",
1344        ] {
1345            assert!(
1346                names.contains(&String::from(expected)),
1347                "missing {expected} in {names:?}"
1348            );
1349        }
1350        // Tick + Order Consumers (2).
1351        let consumer_names: Vec<String> = out
1352            .event_consumer_interfaces
1353            .iter()
1354            .map(|i| i.name.text.clone())
1355            .collect();
1356        assert!(consumer_names.contains(&String::from("TickConsumer")));
1357        assert!(consumer_names.contains(&String::from("OrderConsumer")));
1358    }
1359}