Skip to main content

zerodds_lang_rules/emit/
interface_rust.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! An IDL `interface`, as a complete DDS-RPC 1.0 Rust binding: the
4//! request and reply topic types of §7.5.1.1, a typed client and
5//! service trait, and a dispatcher — all runnable, not only declared.
6//!
7//! `adapter::interface` reads the interface; `emit::interface_cpp` and
8//! `emit::interface_java` write declarations a peer's own runtime gives
9//! a body to. This writes both halves: the topic types get a real
10//! XCDR2 codec, hand-assembled with the same per-field writers
11//! `crate::encode::rust_codec` uses for an ordinary `struct`, and the
12//! client and the dispatcher are built over `zerodds_rpc::requester`
13//! and `zerodds_rpc::replier` — the crate this generated file has to
14//! depend on, named in the preamble every generated interface carries.
15//!
16//! ## Why the topic types get a codec here and nowhere else
17//!
18//! C++, Java and TypeScript declare `Calculator_add_In` and stop: the
19//! octets are a job for a type-support pass none of the three has
20//! written (`emit::interface_cpp`'s own module doc says so). Rust's
21//! generated client and service actually run — `crates/rpc/tests/`
22//! sends a request and reads a reply — and a `Requester<TIn, TOut>`
23//! needs `TIn: DdsType`. So this crosses the boundary `zerodds-rpc →
24//! zerodds-dcps → zerodds-lang-rules` warns about, deliberately: not by
25//! making this *crate* depend on `zerodds-rpc` — it still does not —
26//! but by writing Rust source that does, exactly as
27//! `crates/zerodds-build/src/dds_type.rs` writes an `impl
28//! zerodds_dcps::DdsType` for a type this crate declared.
29//!
30//! ## The wire layout
31//!
32//! DDS-RPC 1.0 §7.5.1.1's Basic Service Mapping: one request topic type
33//! and one reply topic type per interface, each a `header` plus a
34//! `data` union over every operation. `crates/rpc/src/codegen.rs` is
35//! the data model this shape comes from; this crate cannot depend on
36//! it (`zerodds-rpc` depends on `zerodds-dcps` depends on this crate),
37//! so the shape is read off `adapter::interface::Service` directly, the
38//! way `emit::interface_cpp` already does for C++.
39//!
40//! A union becomes a Rust `enum`, one variant per case plus a default
41//! `UnknownOp(u8)` — §7.5.1.1.6 rule 3 and §7.5.1.1.7 rule 3 — and its
42//! `encode`/`decode` write the `long` discriminator
43//! [`interface::operation_hash`]/[`interface::exception_hash`] compute,
44//! then delegate to the case payload's own codec. Every payload type —
45//! an `_In`, an `_Out`, a `_Result`, a raised exception — gets exactly
46//! that: an inherent `encode(&self, w: &mut Writer)` and `decode(r: &mut
47//! Reader)`, member by member, through `crate::encode::write_value_to`
48//! and `crate::encode::read_for` — the same two functions
49//! `crate::encode::rust_codec` calls for a declared `struct`.
50//!
51//! Every synthesized type is `@final`: no `DHEADER`, matching
52//! `crates/rpc/src/common_types.rs`'s own choice for `RequestHeader`
53//! and `ReplyHeader`, and matching the classic-CDR shape DDS-RPC 1.0
54//! predates XTypes 1.3's extensibility kinds with. The encoding is
55//! fixed to XCDR2 little-endian regardless of the file's own
56//! `--encoding`/`--endian` — [`rpc_layout_key`] — because
57//! `zerodds_rpc::requester::Requester::send_request_blocking` calls
58//! `TIn::encode`, the `DdsType` bridge's un-suffixed method, which is
59//! always that one cell.
60//!
61//! ## What this does not do
62//!
63//! It reads `@service(name = "…")`, `@DDSRequestTopic` and
64//! `@DDSReplyTopic` (`annotated_topics_expr`) and hands the names to
65//! `zerodds_rpc::requester::Requester::with_options` as
66//! `AnnotatedTopics`; `crates/rpc/src/topic_naming.rs` composes the topic
67//! names of §7.4.1 and §7.4.2 from them.
68
69mod enhanced;
70pub mod type_information;
71
72use core::fmt;
73use std::collections::BTreeSet;
74
75use zerodds_ast::{
76    Ast, ExceptionNode, ModuleNode, ParamDirection, PrimitiveKind, TypeKind, TypeNode,
77};
78use zerodds_ast_core::Id;
79use zerodds_ast_layout::key::{Encoding, Endianness, LayoutKey};
80use zerodds_lang_cst::common::{Doc, Ident, Literal, Path, Visibility};
81use zerodds_lang_cst::rust;
82
83use crate::adapter::cst::{MaterialiseError, data_derives, rust_type_of};
84use crate::adapter::interface::{
85    self, BindingError, InterfaceError, Operation, RETURN_MEMBER, Raise, Service,
86};
87use crate::encode::{EncodeError, MemberWrite, Position, read_for, write_of, write_value_to};
88
89/// Why an interface did not become a Rust binding.
90#[derive(Debug)]
91pub enum RustInterfaceError {
92    /// The interface itself did not reach the seam.
93    Interface(InterfaceError),
94    /// A referenced type has no Rust spelling.
95    Materialise(MaterialiseError),
96    /// A field's codec could not be built.
97    Codec(EncodeError),
98}
99
100impl From<InterfaceError> for RustInterfaceError {
101    fn from(error: InterfaceError) -> Self {
102        Self::Interface(error)
103    }
104}
105impl From<BindingError> for RustInterfaceError {
106    fn from(error: BindingError) -> Self {
107        match error {
108            BindingError::Interface(error) => Self::Interface(error),
109            BindingError::Materialise(error) => Self::Materialise(error),
110            // Only the C++ column builds codecs through the seam; the
111            // reason is kept.
112            BindingError::Codec(reason) => Self::Materialise(MaterialiseError::UnknownType {
113                type_name: format!("a codec: {reason}"),
114            }),
115        }
116    }
117}
118impl From<MaterialiseError> for RustInterfaceError {
119    fn from(error: MaterialiseError) -> Self {
120        Self::Materialise(error)
121    }
122}
123impl From<EncodeError> for RustInterfaceError {
124    fn from(error: EncodeError) -> Self {
125        Self::Codec(error)
126    }
127}
128
129impl fmt::Display for RustInterfaceError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Self::Interface(error) => write!(f, "{error}"),
133            Self::Materialise(error) => write!(f, "{error:?}"),
134            Self::Codec(error) => write!(f, "{error}"),
135        }
136    }
137}
138
139/// The `LayoutKey` every synthesized topic type's codec is built under.
140/// See the module doc.
141#[must_use]
142pub fn rpc_layout_key() -> LayoutKey {
143    LayoutKey::data(Encoding::PlainCdr2, Endianness::Little)
144}
145
146/// The `LayoutKey` of the XCDR1 (classic CDR) codec of every synthesized
147/// topic type, the second pair `CodecVariant::XCDR1` writes. DDS-RPC 1.0
148/// §7.5.1.1: the Basic Service Mapping is for DDS 1.3.
149#[must_use]
150pub fn rpc_layout_key_xcdr1() -> LayoutKey {
151    LayoutKey::data(Encoding::PlainCdr1, Endianness::Little)
152}
153
154/// Which of a topic type's two codec pairs a function writes or calls.
155///
156/// The XCDR2 pair has the bare names `encode`/`decode`; the XCDR1 pair
157/// carries `crate::encode::SECONDARY_SUFFIX` (`encode_xcdr1`,
158/// `decode_xcdr1`), as `crate::encode::rust_codec_pair` names the second
159/// pair of a declared type, so a member that is a declared type calls the
160/// pair of its own version.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162enum CodecVariant {
163    Xcdr2,
164    Xcdr1,
165}
166
167impl CodecVariant {
168    fn suffix(self) -> &'static str {
169        match self {
170            Self::Xcdr2 => "",
171            Self::Xcdr1 => crate::encode::SECONDARY_SUFFIX,
172        }
173    }
174
175    fn key(self) -> LayoutKey {
176        match self {
177            Self::Xcdr2 => rpc_layout_key(),
178            Self::Xcdr1 => rpc_layout_key_xcdr1(),
179        }
180    }
181
182    fn encode_name(self) -> String {
183        format!("encode{}", self.suffix())
184    }
185
186    fn decode_name(self) -> String {
187        format!("decode{}", self.suffix())
188    }
189}
190
191/// The Rust declarations and runnable binding every interface of `ast`
192/// produces, each paired with the module it belongs in so a caller can
193/// fold it into the same tree `crate::adapter::cst::nest_by_module`
194/// builds for everything else.
195///
196/// # Errors
197/// [`RustInterfaceError`] — the interface did not reach the seam, a
198/// referenced type has no Rust spelling, or a field's codec could not
199/// be built. Never a dropped operation, parameter, attribute or raised
200/// exception.
201pub fn rust_interface_pairs(
202    ast: &Ast,
203) -> Result<Vec<(Id<ModuleNode>, rust::Item)>, RustInterfaceError> {
204    rust_interface_pairs_with(ast, interface::ServiceMapping::Basic)
205}
206
207/// [`rust_interface_pairs`] for the given service mapping.
208///
209/// [`interface::ServiceMapping::Enhanced`] writes the topic types of
210/// DDS-RPC 1.0 §7.5.1.2 — the `@Choice @Autoid` `Request` and `Reply`
211/// and the `In`, `Out` and `Result` structures, with their PL_CDR2 codec
212/// — and a client and a dispatcher that use
213/// `zerodds_rpc::endpoint_options::RpcMapping::Enhanced`.
214///
215/// # Errors
216/// As [`rust_interface_pairs`].
217pub fn rust_interface_pairs_with(
218    ast: &Ast,
219    mapping: interface::ServiceMapping,
220) -> Result<Vec<(Id<ModuleNode>, rust::Item)>, RustInterfaceError> {
221    let mut out = Vec::new();
222    let capable = xcdr1_capable_types(ast);
223    for service in interface::services(ast)? {
224        if mapping.is_enhanced() {
225            enhanced::declare(ast, &Service { mapping, ..service }, &mut out)?;
226        } else {
227            declare(ast, &service, &capable, &mut out)?;
228        }
229    }
230    Ok(out)
231}
232
233/// The declared types that have an XCDR1 codec pair
234/// (`crate::encode::rust_codec_pair` under [`rpc_layout_key_xcdr1`]).
235/// `crate::generate` emits those pairs for a model with interfaces, and the
236/// topic types' XCDR1 codec calls them.
237#[must_use]
238pub fn xcdr1_capable_types(ast: &Ast) -> BTreeSet<Id<TypeNode>> {
239    ast.types()
240        .filter(|(id, node)| {
241            !node.name.is_empty()
242                && crate::encode::has_codec(&node.kind)
243                && crate::encode::rust_codec_pair(ast, *id, rpc_layout_key_xcdr1()).is_ok()
244        })
245        .map(|(id, _)| id)
246        .collect()
247}
248
249/// The most distinct declared types one reachability walk visits. The
250/// visited set bounds the recursion; past this many the walk answers
251/// "not closed", which withholds the XCDR1 form and makes the Basic default
252/// fail with an error rather than send wrong octets.
253const MAX_REACHABLE_TYPES: usize = 4096;
254
255/// Whether every declared type reachable from `target` has an XCDR1 pair.
256///
257/// zerodds-lint: recursion-depth 4096 (each type is entered once; the
258/// visited set is capped at `MAX_REACHABLE_TYPES`)
259pub(crate) fn type_closed(
260    ast: &Ast,
261    target: Id<TypeNode>,
262    capable: &BTreeSet<Id<TypeNode>>,
263    seen: &mut BTreeSet<Id<TypeNode>>,
264) -> bool {
265    if !seen.insert(target) {
266        return true;
267    }
268    if seen.len() > MAX_REACHABLE_TYPES {
269        return false;
270    }
271    let Ok(node) = ast.type_(target) else {
272        return false;
273    };
274    if crate::encode::has_codec(&node.kind) && !capable.contains(&target) {
275        return false;
276    }
277    let reaches = |reference: &zerodds_ast_core::Ref<TypeNode>,
278                   seen: &mut BTreeSet<Id<TypeNode>>| {
279        reference
280            .resolved()
281            .is_some_and(|id| type_closed(ast, id, capable, seen))
282    };
283    let member_closed = |member: Id<zerodds_ast::MemberNode>, seen: &mut BTreeSet<Id<TypeNode>>| {
284        ast.member(member)
285            .is_ok_and(|member| reaches(&member.type_, seen))
286    };
287    match &node.kind {
288        TypeKind::Structure { base, members } => {
289            base.as_ref().is_none_or(|base| reaches(base, seen))
290                && members.iter().all(|m| member_closed(*m, seen))
291        }
292        TypeKind::Union {
293            discriminator,
294            cases,
295            ..
296        } => {
297            reaches(discriminator, seen)
298                && cases.iter().all(|case| {
299                    ast.case(*case)
300                        .is_ok_and(|case| member_closed(case.member, seen))
301                })
302        }
303        TypeKind::Alias { aliased } => reaches(aliased, seen),
304        TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
305            reaches(element, seen)
306        }
307        TypeKind::Map { key, value, .. } => reaches(key, seen) && reaches(value, seen),
308        TypeKind::ValueBox { boxed } => reaches(boxed, seen),
309        TypeKind::Valuetype { bases, state, .. } => {
310            bases.iter().all(|b| reaches(b, seen)) && state.iter().all(|m| member_closed(*m, seen))
311        }
312        _ => true,
313    }
314}
315
316fn fields_closed(ast: &Ast, fields: &[FieldSpec], capable: &BTreeSet<Id<TypeNode>>) -> bool {
317    fields.iter().all(|field| match field {
318        FieldSpec::Octet(_) => true,
319        FieldSpec::Real(_, target) => type_closed(ast, *target, capable, &mut BTreeSet::new()),
320    })
321}
322
323fn declare(
324    ast: &Ast,
325    service: &Service,
326    capable: &BTreeSet<Id<TypeNode>>,
327    out: &mut Vec<(Id<ModuleNode>, rust::Item)>,
328) -> Result<(), RustInterfaceError> {
329    let module = service.module;
330    let ops = service.all_operations();
331
332    // The exceptions this interface's operations raise, each declared
333    // (as a plain struct, by `crate::adapter::cst`'s own walk of
334    // `ast.exceptions()`) but with no codec of its own — nothing before
335    // this reads an exception outside a C++/Java header. One codec per
336    // exception, deduplicated, in the exception's own module: two
337    // operations that raise the same exception must not each get their
338    // own `impl DivByZero`, which is `error[E0592]`.
339    //
340    // Each codec has a second pair for XCDR1 (classic CDR, the Basic
341    // mapping's encoding, DDS-RPC 1.0 §7.5.1.1), written only where every
342    // declared type it reaches has one: `xcdr1_ok` records which of them
343    // does, and a topic type without an XCDR1 pair leaves the `DdsType`
344    // XCDR1 methods at the trait's refusing defaults.
345    let mut exception_ok: Vec<(Id<ExceptionNode>, bool)> = Vec::new();
346    for raise in exceptions_of(&ops) {
347        let exc_module = exception_module(ast, raise.id)?;
348        out.push((exc_module, exception_codec_item(ast, raise.id)?));
349        let ok = exception_fields_closed(ast, raise.id, capable)?;
350        if ok {
351            out.push((
352                exc_module,
353                exception_codec_item_with(ast, raise.id, CodecVariant::Xcdr1)?,
354            ));
355        }
356        exception_ok.push((raise.id, ok));
357    }
358
359    let mut in_ok: Vec<bool> = Vec::new();
360    let mut result_ok: Vec<bool> = Vec::new();
361    for op in &ops {
362        out.push((module, rust::Item::Struct(in_struct(ast, service, op)?)));
363        let in_name = interface::in_struct_name(&service.name, &op.name);
364        out.push((
365            module,
366            struct_codec_item(ast, module, &in_name, &in_fields(op))?,
367        ));
368        let ok = fields_closed(ast, &in_fields(op), capable);
369        if ok {
370            out.push((
371                module,
372                struct_codec_item_with(ast, module, &in_name, &in_fields(op), CodecVariant::Xcdr1)?,
373            ));
374        }
375        in_ok.push(ok);
376        out.push((module, rust::Item::Struct(out_struct(ast, service, op)?)));
377        let out_name = interface::out_struct_name(&service.name, &op.name);
378        out.push((
379            module,
380            struct_codec_item(ast, module, &out_name, &out_fields(op))?,
381        ));
382        let out_ok = fields_closed(ast, &out_fields(op), capable);
383        if out_ok {
384            out.push((
385                module,
386                struct_codec_item_with(
387                    ast,
388                    module,
389                    &out_name,
390                    &out_fields(op),
391                    CodecVariant::Xcdr1,
392                )?,
393            ));
394        }
395        let raised_ok = op
396            .raises
397            .iter()
398            .all(|raise| exception_ok.iter().any(|(id, ok)| *id == raise.id && *ok));
399        result_ok.push(out_ok && raised_ok);
400    }
401
402    for (op, ok) in ops.iter().zip(&result_ok) {
403        let (item, codec) = result_union(ast, module, service, op)?;
404        out.push((module, item));
405        out.push((module, codec));
406        if *ok {
407            out.push((
408                module,
409                union_codec_with(
410                    &interface::result_union_name(&service.name, &op.name),
411                    &result_union_cases(ast, module, service, op)?,
412                    None,
413                    CodecVariant::Xcdr1,
414                )?,
415            ));
416        }
417    }
418
419    let (call_item, call_codec) = call_union(service, &ops)?;
420    out.push((module, call_item));
421    out.push((module, call_codec));
422    let call_ok = in_ok.iter().all(|ok| *ok);
423    if call_ok {
424        out.push((
425            module,
426            union_codec_with(
427                &interface::call_union_name(&service.name),
428                &call_union_cases(service, &ops),
429                Some(UNKNOWN_OP_VARIANT),
430                CodecVariant::Xcdr1,
431            )?,
432        ));
433    }
434
435    let (return_item, return_codec) = return_union(service, &ops)?;
436    out.push((module, return_item));
437    out.push((module, return_codec));
438    let return_ok = ops.iter().zip(&result_ok).all(|(_, ok)| *ok);
439    if return_ok {
440        out.push((
441            module,
442            union_codec_with(
443                &interface::return_union_name(&service.name),
444                &return_union_cases(service, &ops),
445                Some(UNKNOWN_OP_VARIANT),
446                CodecVariant::Xcdr1,
447            )?,
448        ));
449    }
450
451    out.push((module, rust::Item::Struct(request_struct(service))));
452    out.push((module, rust::Item::Struct(reply_struct(service))));
453
454    let exceptions = exceptions_of(&ops);
455    out.push((
456        module,
457        rust::Item::Enum(error_enum(ast, module, service, &exceptions)?),
458    ));
459
460    // `zerodds_rpc::requester::Requester<TIn, TOut>` and
461    // `zerodds_rpc::replier::Replier<TIn, TOut>` both require
462    // `TIn/TOut: DdsType` — the two types they are instantiated with
463    // here are `{I}_Call` and `{I}_Return`, never the per-operation
464    // payloads, which reach the wire only nested inside one of those
465    // two.
466    out.push((
467        module,
468        dds_type_impl(
469            &interface::call_union_name(&service.name),
470            &interface::call_union_name(&service.name),
471            call_ok,
472        ),
473    ));
474    out.push((
475        module,
476        dds_type_impl(
477            &interface::return_union_name(&service.name),
478            &interface::return_union_name(&service.name),
479            return_ok,
480        ),
481    ));
482
483    if service.is_remote() {
484        out.push((module, rust::Item::Trait(service_trait(ast, service)?)));
485        out.push((module, rust::Item::Struct(client_struct(ast, service)?)));
486        out.push((module, client_impl(ast, service)?));
487        out.push((module, rust::Item::Struct(dispatcher_struct(service))));
488        for item in dispatcher_impl(ast, service)? {
489            out.push((module, item));
490        }
491    }
492
493    Ok(())
494}
495
496// == field description ===================================================
497
498/// One field of a synthesized structure: either a real IDL type, or one
499/// of the three placeholder octets §7.5.1.1.1 declares
500/// (`UnusedMember`/`UnknownOperation`), which have no `Id<TypeNode>` to
501/// read a writer off.
502enum FieldSpec {
503    Real(String, Id<TypeNode>),
504    Octet(String),
505}
506
507fn in_fields(op: &Operation) -> Vec<FieldSpec> {
508    in_fields_styled(op, interface::EmptyOperationStyle::DummyMember)
509}
510
511/// [`in_fields`] with the Enhanced mapping's choice for an operation that
512/// has nothing to carry (§7.5.1.2.1.3): the `dummy` member, or no member.
513fn in_fields_styled(op: &Operation, empty: interface::EmptyOperationStyle) -> Vec<FieldSpec> {
514    let mut out: Vec<FieldSpec> = op
515        .inputs()
516        .map(|p| FieldSpec::Real(rust::escape_keyword(&p.name), p.type_))
517        .collect();
518    if out.is_empty() && empty == interface::EmptyOperationStyle::DummyMember {
519        out.push(FieldSpec::Octet(interface::DUMMY_MEMBER.to_string()));
520    }
521    out
522}
523
524fn out_fields(op: &Operation) -> Vec<FieldSpec> {
525    out_fields_styled(op, interface::EmptyOperationStyle::DummyMember)
526}
527
528/// [`out_fields`] with the Enhanced mapping's choice for an operation that
529/// has nothing to carry.
530fn out_fields_styled(op: &Operation, empty: interface::EmptyOperationStyle) -> Vec<FieldSpec> {
531    let mut out: Vec<FieldSpec> = op
532        .outputs()
533        .map(|p| FieldSpec::Real(rust::escape_keyword(&p.name), p.type_))
534        .collect();
535    if let Some(returns) = op.returns {
536        let taken: Vec<&str> = out
537            .iter()
538            .map(|f| match f {
539                FieldSpec::Real(name, _) | FieldSpec::Octet(name) => name.as_str(),
540            })
541            .collect();
542        out.push(FieldSpec::Real(return_member_name(&taken), returns));
543    }
544    if out.is_empty() && empty == interface::EmptyOperationStyle::DummyMember {
545        out.push(FieldSpec::Octet(interface::DUMMY_MEMBER.to_string()));
546    }
547    out
548}
549
550/// DDS-RPC 1.0 §7.5.1.1.5 rule 3b's collision rule, restated for Rust:
551/// an `out` parameter already named `return_` pushes the return value
552/// to `return_1`, `return_2`, ….
553fn return_member_name(taken: &[&str]) -> String {
554    if !taken.contains(&RETURN_MEMBER) {
555        return RETURN_MEMBER.to_string();
556    }
557    for n in 1..=i32::MAX {
558        let candidate = format!("{RETURN_MEMBER}{n}");
559        if !taken.contains(&candidate.as_str()) {
560            return candidate;
561        }
562    }
563    format!("{RETURN_MEMBER}_")
564}
565
566fn field(name: &str, type_: rust::Type, doc: &str) -> rust::Field {
567    rust::Field {
568        doc: Doc::line(doc),
569        attrs: Vec::new(),
570        visibility: Visibility::Public,
571        name: Ident::new(name),
572        type_,
573    }
574}
575
576/// `#[allow(non_camel_case_types)]` — every wire-named type this module
577/// declares (`{I}_{op}_In`, `{I}_Call`, …) carries the spelling
578/// DDS-RPC 1.0 §7.5.1.1.4-7 fixes, not Rust's naming convention; the
579/// generated file answers the lint itself rather than asking a
580/// consumer to.
581fn allow_non_camel_case() -> rust::Attr {
582    rust::Attr {
583        path: Path::single("allow"),
584        args: vec![rust::AttrArg::Word(Ident::new("non_camel_case_types"))],
585    }
586}
587
588fn rust_field_type(
589    ast: &Ast,
590    from: Id<ModuleNode>,
591    spec: &FieldSpec,
592) -> Result<rust::Type, RustInterfaceError> {
593    Ok(match spec {
594        FieldSpec::Real(_, target) => rust_type_of(ast, from, Some(*target))?,
595        FieldSpec::Octet(_) => rust::Type::Primitive(rust::Primitive::U8),
596    })
597}
598
599fn struct_of(name: &str, fields: Vec<rust::Field>, doc: Doc) -> rust::Struct {
600    rust::Struct {
601        body_text: zerodds_lang_cst::common::BodyText::none(),
602        doc,
603        attrs: vec![allow_non_camel_case()],
604        derives: data_derives(),
605        visibility: Visibility::Public,
606        name: Ident::new(name),
607        generics: Vec::new(),
608        body: rust::StructBody::Named(fields),
609    }
610}
611
612fn in_struct(
613    ast: &Ast,
614    service: &Service,
615    op: &Operation,
616) -> Result<rust::Struct, RustInterfaceError> {
617    let mut fields = Vec::new();
618    for spec in in_fields_styled(op, service.mapping.empty_style()) {
619        let type_ = rust_field_type(ast, service.module, &spec)?;
620        let name = match &spec {
621            FieldSpec::Real(name, _) | FieldSpec::Octet(name) => name.clone(),
622        };
623        let doc = field_doc(&spec);
624        fields.push(field(&name, type_, &doc));
625    }
626    Ok(struct_of(
627        &interface::in_struct_name(&service.name, &op.name),
628        fields,
629        Doc::line("The `in` and `inout` parameters of one call. DDS-RPC 1.0 §7.5.1.1.4."),
630    ))
631}
632
633/// The doc one field of a synthesized structure carries — the IDL
634/// parameter it came from, or the placeholder rule that put it there.
635fn field_doc(spec: &FieldSpec) -> String {
636    match spec {
637        FieldSpec::Real(name, _) => format!("The `{name}` parameter."),
638        FieldSpec::Octet(name) => format!(
639            "`{name}`: the operation carries nothing else here. DDS-RPC 1.0 §7.5.1.1.4 rule 3c / \
640             §7.5.1.1.5 rule 3c."
641        ),
642    }
643}
644
645fn out_struct(
646    ast: &Ast,
647    service: &Service,
648    op: &Operation,
649) -> Result<rust::Struct, RustInterfaceError> {
650    let mut fields = Vec::new();
651    for spec in out_fields_styled(op, service.mapping.empty_style()) {
652        let type_ = rust_field_type(ast, service.module, &spec)?;
653        let name = match &spec {
654            FieldSpec::Real(name, _) | FieldSpec::Octet(name) => name.clone(),
655        };
656        let doc = if name == RETURN_MEMBER || name.starts_with(RETURN_MEMBER) {
657            "The operation's return value. DDS-RPC 1.0 §7.5.1.1.5 rule 3b.".to_string()
658        } else {
659            field_doc(&spec)
660        };
661        fields.push(field(&name, type_, &doc));
662    }
663    Ok(struct_of(
664        &interface::out_struct_name(&service.name, &op.name),
665        fields,
666        Doc::line(
667            "The return value and the `out`/`inout` parameters of one call. DDS-RPC 1.0 §7.5.1.1.5.",
668        ),
669    ))
670}
671
672fn request_struct(service: &Service) -> rust::Struct {
673    struct_of(
674        &interface::request_type_name(&service.name),
675        vec![
676            field(
677                "header",
678                rust::Type::named(Path::new(&["zerodds_rpc", "common_types", "RequestHeader"])),
679                "The request header. DDS-RPC 1.0 §7.5.1.1.6 rule 2.",
680            ),
681            field(
682                interface::DATA_MEMBER,
683                rust::Type::named(Path::single(&interface::call_union_name(&service.name))),
684                "The call, as a union over every operation. DDS-RPC 1.0 §7.5.1.1.6 rule 2.",
685            ),
686        ],
687        Doc::lines(&[
688            "The request topic type. DDS-RPC 1.0 §7.5.1.1.6.",
689            "",
690            "Declared for wire-shape parity with the C++ and Java bindings; \
691             `zerodds_rpc::requester::Requester` frames the header itself \
692             (`crates/rpc/src/wire_codec.rs`) and takes the `data` type — \
693             this interface's `_Call`/`_Return` — as its own two type \
694             parameters, so this type is never constructed by the \
695             generated client or dispatcher.",
696        ]),
697    )
698}
699
700fn reply_struct(service: &Service) -> rust::Struct {
701    struct_of(
702        &interface::reply_type_name(&service.name),
703        vec![
704            field(
705                "header",
706                rust::Type::named(Path::new(&["zerodds_rpc", "common_types", "ReplyHeader"])),
707                "The reply header. DDS-RPC 1.0 §7.5.1.1.7 rule 3.",
708            ),
709            field(
710                interface::DATA_MEMBER,
711                rust::Type::named(Path::single(&interface::return_union_name(&service.name))),
712                "The outcome, as a union over every non-`oneway` operation. \
713                 DDS-RPC 1.0 §7.5.1.1.7 rule 3.",
714            ),
715        ],
716        Doc::line("The reply topic type. DDS-RPC 1.0 §7.5.1.1.7. See `Request`'s own doc."),
717    )
718}
719
720// == codec ================================================================
721
722fn position_for(ast: &Ast, target: Id<TypeNode>) -> Position {
723    Position {
724        behaviour: ast.effective_try_construct(None, Some(target)),
725        written: None,
726    }
727}
728
729fn member_write_with(
730    ast: &Ast,
731    from: Id<ModuleNode>,
732    target: Id<TypeNode>,
733    variant: CodecVariant,
734) -> MemberWrite {
735    write_of(
736        ast,
737        from,
738        target,
739        variant.key(),
740        variant.suffix(),
741        position_for(ast, target),
742    )
743}
744
745/// The length code of the `EMHEADER` of a member of type `target`
746/// (XTypes 1.3 §7.4.3.4.2) in the Enhanced Service Mapping's PL_CDR2 form.
747///
748/// The Rust codec decides it from the member's write form; the C++ and the
749/// Java codec of the same structures ask here, so the three write the same
750/// octets.
751pub(crate) fn emheader_length_code(ast: &Ast, from: Id<ModuleNode>, target: Id<TypeNode>) -> u8 {
752    enhanced::length_code(&member_write_with(ast, from, target, CodecVariant::Xcdr2))
753}
754
755fn self_field(name: &str) -> rust::Expr {
756    rust::Expr::Field {
757        receiver: Box::new(rust::Expr::Path(Path::single("self"))),
758        name: Ident::new(name),
759    }
760}
761
762/// One structure's `encode`/`decode`, member by member, through
763/// [`write_value_to`] and [`read_for`] — the same two functions
764/// `crate::encode::rust_codec` calls for a declared `struct`. `@final`
765/// throughout: no `DHEADER`, matching the module doc.
766fn struct_codec_item(
767    ast: &Ast,
768    from: Id<ModuleNode>,
769    type_name: &str,
770    fields: &[FieldSpec],
771) -> Result<rust::Item, RustInterfaceError> {
772    struct_codec_item_with(ast, from, type_name, fields, CodecVariant::Xcdr2)
773}
774
775/// [`struct_codec_item`] for one of the two codec pairs.
776fn struct_codec_item_with(
777    ast: &Ast,
778    from: Id<ModuleNode>,
779    type_name: &str,
780    fields: &[FieldSpec],
781    variant: CodecVariant,
782) -> Result<rust::Item, RustInterfaceError> {
783    let mut encode_statements = Vec::new();
784    let mut decode_statements = Vec::new();
785    let mut struct_fields = Vec::new();
786    for spec in fields {
787        let (name, kind) = match spec {
788            FieldSpec::Real(name, target) => {
789                (name.clone(), member_write_with(ast, from, *target, variant))
790            }
791            FieldSpec::Octet(name) => (name.clone(), MemberWrite::Primitive(PrimitiveKind::Octet)),
792        };
793        encode_statements.push(write_value_to("w", &name, &kind, self_field(&name), false)?);
794        decode_statements.push(rust::Statement::Let {
795            name: Ident::new(&name),
796            type_: None,
797            value: Some(rust::Expr::Try(Box::new(read_for(&name, &kind)?))),
798            mutable: false,
799        });
800        struct_fields.push((Ident::new(&name), rust::Expr::Path(Path::single(&name))));
801    }
802    if fields.is_empty() {
803        // An `@Empty` structure (§7.5.1.2.1.3) writes and reads nothing; the
804        // writer and the reader are still parameters.
805        for (statements, name) in [(&mut encode_statements, "w"), (&mut decode_statements, "r")] {
806            statements.push(rust::Statement::Let {
807                name: Ident::new("_"),
808                type_: None,
809                value: Some(rust::Expr::Ref {
810                    target: Box::new(rust::Expr::Path(Path::single(name))),
811                    mutable: false,
812                }),
813                mutable: false,
814            });
815        }
816    }
817    encode_statements.push(rust::Statement::Tail(ok_unit()));
818    decode_statements.push(rust::Statement::Tail(rust::Expr::Call {
819        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
820        args: vec![rust::Expr::StructLit {
821            path: Path::single("Self"),
822            fields: struct_fields,
823            base: None,
824        }],
825    }));
826    Ok(rust::Item::Impl(rust::Impl {
827        doc: Doc::none(),
828        attrs: Vec::new(),
829        generics: Vec::new(),
830        trait_: None,
831        self_type: rust::Type::named(Path::single(type_name)),
832        items: vec![
833            encode_fn_item_named(&variant.encode_name(), type_name, encode_statements),
834            decode_fn_item_named(&variant.decode_name(), type_name, decode_statements),
835        ],
836    }))
837}
838
839fn ok_unit() -> rust::Expr {
840    rust::Expr::Call {
841        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
842        args: vec![rust::Expr::Tuple(Vec::new())],
843    }
844}
845
846fn writer_ref_type() -> rust::Type {
847    rust::Type::Ref {
848        target: Box::new(rust::Type::named(Path::single("Writer"))),
849        mutable: true,
850        lifetime: None,
851    }
852}
853
854fn reader_ref_type() -> rust::Type {
855    rust::Type::Ref {
856        target: Box::new(rust::Type::named(Path::single("Reader"))),
857        mutable: true,
858        lifetime: None,
859    }
860}
861
862fn wire_error_result(ok: rust::Type) -> rust::Type {
863    rust::Type::generic(
864        Path::single("Result"),
865        vec![ok, rust::Type::named(Path::single("WireError"))],
866    )
867}
868
869fn encode_fn_item_named(
870    name: &str,
871    type_name: &str,
872    statements: Vec<rust::Statement>,
873) -> rust::Item {
874    rust::Item::Function(rust::Function {
875        doc: Doc::line(&format!(
876            "Writes `{type_name}` to `w` with `crate::encode::rust_codec`'s CDR encoding."
877        )),
878        attrs: Vec::new(),
879        visibility: Visibility::Public,
880        name: Ident::new(name),
881        generics: Vec::new(),
882        params: vec![
883            rust::Param {
884                name: None,
885                type_: rust::Type::unit(),
886                receiver: Some(rust::Receiver::Ref),
887            },
888            rust::Param {
889                name: Some(Ident::new("w")),
890                type_: writer_ref_type(),
891                receiver: None,
892            },
893        ],
894        returns: Some(wire_error_result(rust::Type::unit())),
895        body: Some(rust::Block { statements }),
896    })
897}
898
899fn decode_fn_item_named(
900    name: &str,
901    type_name: &str,
902    statements: Vec<rust::Statement>,
903) -> rust::Item {
904    rust::Item::Function(rust::Function {
905        doc: Doc::line(&format!(
906            "Reads a `{type_name}` from `r` with `crate::encode::rust_codec`'s CDR decoding."
907        )),
908        attrs: Vec::new(),
909        visibility: Visibility::Public,
910        name: Ident::new(name),
911        generics: Vec::new(),
912        params: vec![rust::Param {
913            name: Some(Ident::new("r")),
914            type_: reader_ref_type(),
915            receiver: None,
916        }],
917        returns: Some(wire_error_result(rust::Type::named(Path::single("Self")))),
918        body: Some(rust::Block { statements }),
919    })
920}
921
922// -- exceptions -----------------------------------------------------------
923
924fn exceptions_of(ops: &[Operation]) -> Vec<Raise> {
925    let mut out: Vec<Raise> = Vec::new();
926    for op in ops {
927        for raise in &op.raises {
928            if !out.iter().any(|seen| seen.id == raise.id) {
929                out.push(raise.clone());
930            }
931        }
932    }
933    out
934}
935
936fn exception_module(
937    ast: &Ast,
938    id: Id<ExceptionNode>,
939) -> Result<Id<ModuleNode>, RustInterfaceError> {
940    let node = ast.exception(id).map_err(|_| InterfaceError::NoNode {
941        kind: "exception",
942        at: "the model".to_string(),
943    })?;
944    Ok(node.module)
945}
946
947/// The path to an exception's already-declared Rust struct, relative to
948/// `from`. `crate::adapter::cst::relative_path` cannot be reused
949/// directly — it takes a `&TypeNode` and an exception is not one — so
950/// this walks the same shared-prefix logic over
951/// `adapter::interface::module_chain`.
952fn exception_path(
953    ast: &Ast,
954    from: Id<ModuleNode>,
955    id: Id<ExceptionNode>,
956) -> Result<Path, RustInterfaceError> {
957    let node = ast.exception(id).map_err(|_| InterfaceError::NoNode {
958        kind: "exception",
959        at: "the model".to_string(),
960    })?;
961    relative_module_path(ast, from, node.module, &node.name)
962}
963
964/// A declared type's path as written from inside module `from`, given
965/// only the module it sits in and its own name.
966///
967/// `crate::adapter::cst::relative_path` cannot be reused directly — it
968/// takes a `&TypeNode`, and neither an exception nor an interface (an
969/// RPC trait's base) is one. Same logic, over
970/// `adapter::interface::module_chain`.
971fn relative_module_path(
972    ast: &Ast,
973    from: Id<ModuleNode>,
974    target_module: Id<ModuleNode>,
975    leaf: &str,
976) -> Result<Path, RustInterfaceError> {
977    let here = interface::module_chain(ast, from)?;
978    let there = interface::module_chain(ast, target_module)?;
979    let shared = here
980        .iter()
981        .zip(there.iter())
982        .take_while(|(a, b)| a.0 == b.0)
983        .count();
984    let mut segments: Vec<String> = Vec::new();
985    for _ in shared..here.len() {
986        segments.push("super".to_string());
987    }
988    for (_, name) in there.iter().skip(shared) {
989        segments.push(rust::escape_keyword(name));
990    }
991    segments.push(rust::escape_keyword(leaf));
992    let borrowed: Vec<&str> = segments.iter().map(String::as_str).collect();
993    Ok(Path::new(&borrowed))
994}
995
996fn exception_codec_item(
997    ast: &Ast,
998    id: Id<ExceptionNode>,
999) -> Result<rust::Item, RustInterfaceError> {
1000    exception_codec_item_with(ast, id, CodecVariant::Xcdr2)
1001}
1002
1003/// Whether every declared type an exception's members reach has an XCDR1
1004/// pair.
1005fn exception_fields_closed(
1006    ast: &Ast,
1007    id: Id<ExceptionNode>,
1008    capable: &BTreeSet<Id<TypeNode>>,
1009) -> Result<bool, RustInterfaceError> {
1010    let node = ast.exception(id).map_err(|_| InterfaceError::NoNode {
1011        kind: "exception",
1012        at: "the model".to_string(),
1013    })?;
1014    Ok(node.members.iter().all(|member| {
1015        ast.member(*member).is_ok_and(|member| {
1016            member
1017                .type_
1018                .resolved()
1019                .is_some_and(|target| type_closed(ast, target, capable, &mut BTreeSet::new()))
1020        })
1021    }))
1022}
1023
1024fn exception_codec_item_with(
1025    ast: &Ast,
1026    id: Id<ExceptionNode>,
1027    variant: CodecVariant,
1028) -> Result<rust::Item, RustInterfaceError> {
1029    let node = ast.exception(id).map_err(|_| InterfaceError::NoNode {
1030        kind: "exception",
1031        at: "the model".to_string(),
1032    })?;
1033    let module = node.module;
1034    let mut fields = Vec::new();
1035    for member in &node.members {
1036        let member_node = ast.member(*member).map_err(|_| InterfaceError::NoNode {
1037            kind: "parameter",
1038            at: node.name.clone(),
1039        })?;
1040        let Some(target) = member_node.type_.resolved() else {
1041            return Err(InterfaceError::Unresolved {
1042                construct: "exception member type",
1043                at: format!("{}::{}", node.name, member_node.name),
1044            }
1045            .into());
1046        };
1047        fields.push(FieldSpec::Real(
1048            rust::escape_keyword(&member_node.name),
1049            target,
1050        ));
1051    }
1052    struct_codec_item_with(ast, module, &node.name, &fields, variant)
1053}
1054
1055// == tagged unions =========================================================
1056
1057/// One case of a tagged union: the enum variant name, the path to the
1058/// type its payload carries (already declared, with its own
1059/// `encode`/`decode`), the `long` discriminator DDS-RPC 1.0 assigns it,
1060/// and a doc line.
1061///
1062/// The payload is a [`Path`] and not a [`rust::Type`]: every case here
1063/// is a named struct or a named union, never a primitive or a
1064/// collection, and keeping the path lets [`decode_case_body`] build
1065/// `<Path>::decode(r)` without taking a named type apart again to find
1066/// out what it already is.
1067struct UnionCase {
1068    variant: String,
1069    payload: Path,
1070    discriminator: i32,
1071    doc: String,
1072}
1073
1074/// `${I}_Call`, `${I}_Return` or `${I}_${op}_Result` as a Rust `enum`,
1075/// plus its `encode`/`decode`.
1076///
1077/// `default_variant` is `Some(name)` for the `unknownOp` case DDS-RPC
1078/// 1.0 §7.5.1.1.6 rule 3 and §7.5.1.1.7 rule 3 both require on the
1079/// `Call` and `Return` unions; `None` for the per-operation `Result`
1080/// union of §7.5.1.1.5, which has no such case — an unrecognised
1081/// discriminator there is refused on decode rather than defaulted,
1082/// because there is no spec-given value to construct.
1083fn tagged_union(
1084    name: &str,
1085    doc: Doc,
1086    cases: Vec<UnionCase>,
1087    default_variant: Option<&str>,
1088) -> Result<(rust::Item, rust::Item), RustInterfaceError> {
1089    let mut variants = Vec::new();
1090    for case in &cases {
1091        variants.push(rust::Variant {
1092            doc: Doc::line(&case.doc),
1093            attrs: Vec::new(),
1094            name: Ident::new(&case.variant),
1095            body: rust::VariantBody::Tuple(vec![rust::Type::named(case.payload.clone())]),
1096            discriminant: None,
1097        });
1098    }
1099    if let Some(default_variant) = default_variant {
1100        variants.push(rust::Variant {
1101            doc: Doc::line(
1102                "The default case: a discriminator this binding does not recognise. \
1103                 DDS-RPC 1.0 §7.5.1.1.6 rule 3 / §7.5.1.1.7 rule 3.",
1104            ),
1105            attrs: Vec::new(),
1106            name: Ident::new(default_variant),
1107            body: rust::VariantBody::Tuple(vec![rust::Type::Primitive(rust::Primitive::U8)]),
1108            discriminant: None,
1109        });
1110    }
1111    let item = rust::Item::Enum(rust::Enum {
1112        doc,
1113        attrs: vec![allow_non_camel_case()],
1114        derives: data_derives(),
1115        visibility: Visibility::Public,
1116        name: Ident::new(name),
1117        generics: Vec::new(),
1118        variants,
1119    });
1120
1121    let codec = union_codec_with(name, &cases, default_variant, CodecVariant::Xcdr2)?;
1122    Ok((item, codec))
1123}
1124
1125/// The `encode`/`decode` pair of a tagged union for one of the two codec
1126/// pairs: the `long` discriminator, then the case payload's own pair of
1127/// the same version.
1128fn union_codec_with(
1129    name: &str,
1130    cases: &[UnionCase],
1131    default_variant: Option<&str>,
1132    variant: CodecVariant,
1133) -> Result<rust::Item, RustInterfaceError> {
1134    let mut encode_arms = Vec::new();
1135    let mut decode_arms = Vec::new();
1136    for case in cases {
1137        encode_arms.push(rust::MatchArm {
1138            pattern: rust::Pattern::TupleStruct {
1139                path: Path::new(&["Self", &case.variant]),
1140                elements: vec![rust::Pattern::Binding(Ident::new("value"))],
1141            },
1142            guard: None,
1143            body: encode_case_body(case.discriminator, "value", variant)?,
1144        });
1145        decode_arms.push(rust::MatchArm {
1146            pattern: rust::Pattern::Literal(Literal::int(i128::from(case.discriminator))),
1147            guard: None,
1148            body: decode_case_body(&case.variant, &case.payload, variant),
1149        });
1150    }
1151    if let Some(default_variant) = default_variant {
1152        encode_arms.push(rust::MatchArm {
1153            pattern: rust::Pattern::TupleStruct {
1154                path: Path::new(&["Self", default_variant]),
1155                elements: vec![rust::Pattern::Binding(Ident::new("value"))],
1156            },
1157            guard: None,
1158            body: encode_default_body("value")?,
1159        });
1160        decode_arms.push(rust::MatchArm {
1161            pattern: rust::Pattern::Wildcard,
1162            guard: None,
1163            body: decode_default_body(default_variant)?,
1164        });
1165    } else {
1166        decode_arms.push(rust::MatchArm {
1167            pattern: rust::Pattern::Wildcard,
1168            guard: None,
1169            body: refused_expr("an unrecognised DDS-RPC 1.0 §7.5.1.1.5 Result discriminator"),
1170        });
1171    }
1172
1173    let encode_statements = vec![rust::Statement::Tail(rust::Expr::Match {
1174        scrutinee: Box::new(rust::Expr::Path(Path::single("self"))),
1175        arms: encode_arms,
1176    })];
1177    let decode_statements = vec![
1178        rust::Statement::Let {
1179            name: Ident::new("disc"),
1180            type_: Some(rust::Type::Primitive(rust::Primitive::I32)),
1181            value: Some(rust::Expr::Try(Box::new(read_discriminator()?))),
1182            mutable: false,
1183        },
1184        rust::Statement::Tail(rust::Expr::Match {
1185            scrutinee: Box::new(rust::Expr::Path(Path::single("disc"))),
1186            arms: decode_arms,
1187        }),
1188    ];
1189    let codec = rust::Item::Impl(rust::Impl {
1190        doc: Doc::none(),
1191        attrs: Vec::new(),
1192        generics: Vec::new(),
1193        trait_: None,
1194        self_type: rust::Type::named(Path::single(name)),
1195        items: vec![
1196            encode_fn_item_named(&variant.encode_name(), name, encode_statements),
1197            decode_fn_item_named(&variant.decode_name(), name, decode_statements),
1198        ],
1199    });
1200    Ok(codec)
1201}
1202
1203/// `w.write_i32(&value)?` in shape but never fallible in practice: an
1204/// `Int32` write never refuses. [`write_value_to`] is still called
1205/// through — not hand-assembled — so a change to how the runtime writes
1206/// an `Int32` reaches the discriminator the same call reaches every
1207/// other `Int32` member through, and the `EncodeError` it could in
1208/// principle answer with is reported rather than assumed away.
1209fn discriminator_write(value: rust::Expr) -> Result<rust::Statement, RustInterfaceError> {
1210    Ok(write_value_to(
1211        "w",
1212        "_d",
1213        &MemberWrite::Primitive(PrimitiveKind::Int32),
1214        value,
1215        false,
1216    )?)
1217}
1218
1219fn read_discriminator() -> Result<rust::Expr, RustInterfaceError> {
1220    Ok(read_for(
1221        "_d",
1222        &MemberWrite::Primitive(PrimitiveKind::Int32),
1223    )?)
1224}
1225
1226fn encode_case_body(
1227    discriminator: i32,
1228    binding: &str,
1229    variant: CodecVariant,
1230) -> Result<rust::Expr, RustInterfaceError> {
1231    Ok(rust::Expr::Block(rust::Block {
1232        statements: vec![
1233            discriminator_write(rust::Expr::Literal(Literal::int(i128::from(discriminator))))?,
1234            rust::Statement::Tail(rust::Expr::MethodCall {
1235                receiver: Box::new(rust::Expr::Path(Path::single(binding))),
1236                method: Ident::new(&variant.encode_name()),
1237                args: vec![rust::Expr::Path(Path::single("w"))],
1238            }),
1239        ],
1240    }))
1241}
1242
1243fn encode_default_body(binding: &str) -> Result<rust::Expr, RustInterfaceError> {
1244    Ok(rust::Expr::Block(rust::Block {
1245        statements: vec![
1246            discriminator_write(rust::Expr::Literal(Literal::int(0)))?,
1247            write_value_to(
1248                "w",
1249                "unknown_op",
1250                &MemberWrite::Primitive(PrimitiveKind::Octet),
1251                rust::Expr::Path(Path::single(binding)),
1252                true,
1253            )?,
1254            rust::Statement::Tail(ok_unit()),
1255        ],
1256    }))
1257}
1258
1259fn decode_case_body(variant: &str, payload: &Path, codec: CodecVariant) -> rust::Expr {
1260    rust::Expr::Call {
1261        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1262        args: vec![rust::Expr::Call {
1263            callee: Box::new(rust::Expr::Path(Path::new(&["Self", variant]))),
1264            args: vec![rust::Expr::Try(Box::new(rust::Expr::Call {
1265                callee: Box::new(rust::Expr::Path(payload.join(&codec.decode_name()))),
1266                args: vec![rust::Expr::Path(Path::single("r"))],
1267            }))],
1268        }],
1269    }
1270}
1271
1272fn decode_default_body(default_variant: &str) -> Result<rust::Expr, RustInterfaceError> {
1273    Ok(rust::Expr::Call {
1274        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1275        args: vec![rust::Expr::Call {
1276            callee: Box::new(rust::Expr::Path(Path::new(&["Self", default_variant]))),
1277            args: vec![rust::Expr::Try(Box::new(read_for(
1278                "unknown_op",
1279                &MemberWrite::Primitive(PrimitiveKind::Octet),
1280            )?))],
1281        }],
1282    })
1283}
1284
1285fn refused_expr(why: &str) -> rust::Expr {
1286    rust::Expr::Call {
1287        callee: Box::new(rust::Expr::Path(Path::single("Err"))),
1288        args: vec![rust::Expr::Call {
1289            callee: Box::new(rust::Expr::Path(Path::new(&["WireError", "refused"]))),
1290            args: vec![
1291                rust::Expr::MethodCall {
1292                    receiver: Box::new(rust::Expr::Path(Path::single("r"))),
1293                    method: Ident::new("position"),
1294                    args: Vec::new(),
1295                },
1296                rust::Expr::Literal(Literal::string(why)),
1297            ],
1298        }],
1299    }
1300}
1301
1302// -- Result, Call, Return --------------------------------------------------
1303
1304fn result_union(
1305    ast: &Ast,
1306    module: Id<ModuleNode>,
1307    service: &Service,
1308    op: &Operation,
1309) -> Result<(rust::Item, rust::Item), RustInterfaceError> {
1310    let cases = result_union_cases(ast, module, service, op)?;
1311    let name = interface::result_union_name(&service.name, &op.name);
1312    tagged_union(
1313        &name,
1314        Doc::line(&format!(
1315            "The outcome of `{}`. DDS-RPC 1.0 §7.5.1.1.5.",
1316            op.name
1317        )),
1318        cases,
1319        None,
1320    )
1321}
1322
1323/// The cases of `${I}_${op}_Result`.
1324fn result_union_cases(
1325    ast: &Ast,
1326    module: Id<ModuleNode>,
1327    service: &Service,
1328    op: &Operation,
1329) -> Result<Vec<UnionCase>, RustInterfaceError> {
1330    let mut cases = vec![UnionCase {
1331        variant: "Ok".to_string(),
1332        payload: Path::single(&interface::out_struct_name(&service.name, &op.name)),
1333        discriminator: interface::RETCODE_OK,
1334        doc: "`dds::RETCODE_OK`. DDS-RPC 1.0 §7.5.1.1.5 rule 3a.".to_string(),
1335    }];
1336    for raise in &op.raises {
1337        cases.push(UnionCase {
1338            variant: rust::escape_keyword(&raise.name),
1339            payload: exception_path(ast, module, raise.id)?,
1340            discriminator: interface::exception_hash(&raise.scoped_name),
1341            doc: format!("The `{}` case. DDS-RPC 1.0 §7.5.1.1.5 rule 5.", raise.name),
1342        });
1343    }
1344    Ok(cases)
1345}
1346
1347fn exception_type(
1348    ast: &Ast,
1349    from: Id<ModuleNode>,
1350    id: Id<ExceptionNode>,
1351) -> Result<rust::Type, RustInterfaceError> {
1352    Ok(rust::Type::named(exception_path(ast, from, id)?))
1353}
1354
1355fn call_union(
1356    service: &Service,
1357    ops: &[Operation],
1358) -> Result<(rust::Item, rust::Item), RustInterfaceError> {
1359    let cases = call_union_cases(service, ops);
1360    tagged_union(
1361        &interface::call_union_name(&service.name),
1362        Doc::line("The request union over every operation. DDS-RPC 1.0 §7.5.1.1.6."),
1363        cases,
1364        Some(UNKNOWN_OP_VARIANT),
1365    )
1366}
1367
1368/// The cases of `${I}_Call`.
1369fn call_union_cases(service: &Service, ops: &[Operation]) -> Vec<UnionCase> {
1370    ops.iter()
1371        .map(|op| UnionCase {
1372            variant: variant_name(&op.name),
1373            payload: Path::single(&interface::in_struct_name(&service.name, &op.name)),
1374            discriminator: interface::operation_hash(&op.name),
1375            doc: format!("`{}`. DDS-RPC 1.0 §7.5.1.1.6 rule 5.", op.name),
1376        })
1377        .collect()
1378}
1379
1380fn return_union(
1381    service: &Service,
1382    ops: &[Operation],
1383) -> Result<(rust::Item, rust::Item), RustInterfaceError> {
1384    let cases = return_union_cases(service, ops);
1385    tagged_union(
1386        &interface::return_union_name(&service.name),
1387        Doc::line("The reply union. DDS-RPC 1.0 §7.5.1.1.7."),
1388        cases,
1389        Some(UNKNOWN_OP_VARIANT),
1390    )
1391}
1392
1393/// The cases of `${I}_Return`.
1394fn return_union_cases(service: &Service, ops: &[Operation]) -> Vec<UnionCase> {
1395    ops.iter()
1396        .map(|op| UnionCase {
1397            variant: variant_name(&op.name),
1398            payload: Path::single(&interface::result_union_name(&service.name, &op.name)),
1399            discriminator: interface::operation_hash(&op.name),
1400            doc: format!("`{}`. DDS-RPC 1.0 §7.5.1.1.7 rule 5b.", op.name),
1401        })
1402        .collect()
1403}
1404
1405/// The enum variant DDS-RPC 1.0's `unknownOp` default case takes in
1406/// this binding. `crate::adapter::interface::UNKNOWN_OP_MEMBER` is the
1407/// wire member name the C++ and Java bindings spell; this is its Rust
1408/// variant spelling, named once so the `Call` and `Return` unions agree
1409/// with each other and with `dispatcher_impl`.
1410const UNKNOWN_OP_VARIANT: &str = "UnknownOp";
1411
1412/// The variant an Enhanced `Result` choice holds a member id it does not
1413/// know in: an exception member added by a newer version of the
1414/// interface (DDS-RPC 1.0 §7.7.2.5.6).
1415const UNKNOWN_EX_VARIANT: &str = "UnknownEx";
1416
1417/// A PascalCase enum variant from an IDL name. `get_memory` becomes
1418/// `GetMemory`; a name that is already one word capitalises its first
1419/// letter and keeps the rest, so `add` becomes `Add`.
1420fn variant_name(name: &str) -> String {
1421    let mut out = String::new();
1422    for segment in name.split('_') {
1423        let mut chars = segment.chars();
1424        if let Some(first) = chars.next() {
1425            out.extend(first.to_uppercase());
1426            out.extend(chars);
1427        }
1428    }
1429    if out.is_empty() {
1430        out.push_str("Op");
1431    }
1432    out
1433}
1434
1435fn field_expr(receiver: &str, field_name: &str) -> rust::Expr {
1436    rust::Expr::Field {
1437        receiver: Box::new(rust::Expr::Path(Path::single(receiver))),
1438        name: Ident::new(field_name),
1439    }
1440}
1441
1442fn field_vis(name: &str, type_: rust::Type, visibility: Visibility, doc: &str) -> rust::Field {
1443    rust::Field {
1444        doc: Doc::line(doc),
1445        attrs: Vec::new(),
1446        visibility,
1447        name: Ident::new(name),
1448        type_,
1449    }
1450}
1451
1452fn error_type_name(service: &Service) -> String {
1453    format!("{}Error", service.name)
1454}
1455
1456// == DdsType bridge ========================================================
1457
1458/// `impl zerodds_dcps::dds_type::DdsType for {type_name}`, over the
1459/// inherent `encode`/`decode` [`struct_codec_item`] and [`tagged_union`]
1460/// both already give every synthesized type — the same bridge
1461/// `crates/zerodds-build/src/dds_type.rs` writes for a declared
1462/// `struct`, at the one XCDR2-little-endian cell
1463/// `zerodds_rpc::requester::Requester::send_request_blocking` reads
1464/// (`TIn::encode`, never `encode_be`), so only the trait's two required
1465/// methods are given — every other one keeps the trait's own refusing
1466/// default.
1467/// A `DdsType` impl for a type declared as `type_name` and named in DDS
1468/// `dds_name`: a structure in an IDL module is declared by its simple name
1469/// and named in DDS by its scoped one.
1470pub(crate) fn dds_type_impl(type_name: &str, dds_name: &str, xcdr1: bool) -> rust::Item {
1471    let type_name_const = rust::Item::Const(rust::Const {
1472        doc: Doc::line(
1473            "Informational only: `zerodds_rpc::requester::Requester` never \
1474             registers this as a DDS topic type — it frames the request/reply \
1475             header itself and writes the sample through `DdsType::encode` \
1476             directly. See `crates/rpc/src/wire_codec.rs`.",
1477        ),
1478        attrs: Vec::new(),
1479        visibility: Visibility::Private,
1480        name: Ident::new("TYPE_NAME"),
1481        type_: rust::Type::Ref {
1482            target: Box::new(rust::Type::Primitive(rust::Primitive::Str)),
1483            mutable: false,
1484            lifetime: Some(Ident::new("static")),
1485        },
1486        value: rust::Expr::Literal(Literal::string(dds_name)),
1487    });
1488
1489    let encode_fn = rust::Item::Function(rust::Function {
1490        doc: Doc::none(),
1491        attrs: Vec::new(),
1492        visibility: Visibility::Private,
1493        name: Ident::new("encode"),
1494        generics: Vec::new(),
1495        params: vec![
1496            rust::Param {
1497                name: None,
1498                type_: rust::Type::unit(),
1499                receiver: Some(rust::Receiver::Ref),
1500            },
1501            rust::Param {
1502                name: Some(Ident::new("out")),
1503                type_: rust::Type::Ref {
1504                    target: Box::new(rust::Type::generic(
1505                        Path::single("Vec"),
1506                        vec![rust::Type::Primitive(rust::Primitive::U8)],
1507                    )),
1508                    mutable: true,
1509                    lifetime: None,
1510                },
1511                receiver: None,
1512            },
1513        ],
1514        returns: Some(rust::Type::generic(
1515            Path::single("Result"),
1516            vec![
1517                rust::Type::unit(),
1518                rust::Type::named(Path::new(&["zerodds_dcps", "dds_type", "EncodeError"])),
1519            ],
1520        )),
1521        body: Some(rust::Block {
1522            statements: vec![
1523                rust::Statement::Let {
1524                    name: Ident::new("w"),
1525                    type_: None,
1526                    value: Some(rust::Expr::Call {
1527                        callee: Box::new(rust::Expr::Path(Path::new(&[
1528                            "zerodds_wire",
1529                            "Writer",
1530                            "with_max_align",
1531                        ]))),
1532                        args: vec![
1533                            rust::Expr::Path(Path::new(&["zerodds_wire", "Endianness", "Little"])),
1534                            rust::Expr::Literal(Literal::int(4)),
1535                        ],
1536                    }),
1537                    mutable: true,
1538                },
1539                rust::Statement::Semi(rust::Expr::Try(Box::new(rust::Expr::Call {
1540                    callee: Box::new(rust::Expr::Path(Path::new(&["Self", "encode"]))),
1541                    args: vec![
1542                        rust::Expr::Path(Path::single("self")),
1543                        rust::Expr::Ref {
1544                            target: Box::new(rust::Expr::Path(Path::single("w"))),
1545                            mutable: true,
1546                        },
1547                    ],
1548                }))),
1549                rust::Statement::Semi(rust::Expr::MethodCall {
1550                    receiver: Box::new(rust::Expr::Path(Path::single("out"))),
1551                    method: Ident::new("extend_from_slice"),
1552                    args: vec![rust::Expr::Ref {
1553                        target: Box::new(rust::Expr::MethodCall {
1554                            receiver: Box::new(rust::Expr::Path(Path::single("w"))),
1555                            method: Ident::new("finish"),
1556                            args: Vec::new(),
1557                        }),
1558                        mutable: false,
1559                    }],
1560                }),
1561                rust::Statement::Tail(ok_unit()),
1562            ],
1563        }),
1564    });
1565
1566    let decode_fn = rust::Item::Function(rust::Function {
1567        doc: Doc::none(),
1568        attrs: Vec::new(),
1569        visibility: Visibility::Private,
1570        name: Ident::new("decode"),
1571        generics: Vec::new(),
1572        params: vec![rust::Param {
1573            name: Some(Ident::new("bytes")),
1574            type_: rust::Type::Ref {
1575                target: Box::new(rust::Type::Slice(Box::new(rust::Type::Primitive(
1576                    rust::Primitive::U8,
1577                )))),
1578                mutable: false,
1579                lifetime: None,
1580            },
1581            receiver: None,
1582        }],
1583        returns: Some(rust::Type::generic(
1584            Path::single("Result"),
1585            vec![
1586                rust::Type::named(Path::single("Self")),
1587                rust::Type::named(Path::new(&["zerodds_dcps", "dds_type", "DecodeError"])),
1588            ],
1589        )),
1590        body: Some(rust::Block {
1591            statements: vec![
1592                rust::Statement::Let {
1593                    name: Ident::new("r"),
1594                    type_: None,
1595                    value: Some(rust::Expr::Call {
1596                        callee: Box::new(rust::Expr::Path(Path::new(&[
1597                            "zerodds_wire",
1598                            "Reader",
1599                            "with_max_align",
1600                        ]))),
1601                        args: vec![
1602                            rust::Expr::Path(Path::single("bytes")),
1603                            rust::Expr::Path(Path::new(&["zerodds_wire", "Endianness", "Little"])),
1604                            rust::Expr::Literal(Literal::int(4)),
1605                        ],
1606                    }),
1607                    mutable: true,
1608                },
1609                rust::Statement::Tail(rust::Expr::Call {
1610                    callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1611                    args: vec![rust::Expr::Try(Box::new(rust::Expr::Call {
1612                        callee: Box::new(rust::Expr::Path(Path::new(&["Self", "decode"]))),
1613                        args: vec![rust::Expr::Ref {
1614                            target: Box::new(rust::Expr::Path(Path::single("r"))),
1615                            mutable: true,
1616                        }],
1617                    }))],
1618                }),
1619            ],
1620        }),
1621    });
1622
1623    rust::Item::Impl(rust::Impl {
1624        doc: Doc::none(),
1625        attrs: Vec::new(),
1626        generics: Vec::new(),
1627        trait_: Some(rust::Type::named(Path::new(&[
1628            "zerodds_dcps",
1629            "dds_type",
1630            "DdsType",
1631        ]))),
1632        self_type: rust::Type::named(Path::single(type_name)),
1633        items: {
1634            let mut items = vec![type_name_const, encode_fn, decode_fn];
1635            // The XCDR1 methods only for a type that has the XCDR1 pair:
1636            // otherwise they keep the trait's refusing defaults, so a
1637            // Basic endpoint on classic CDR says why it cannot send.
1638            if xcdr1 {
1639                items.extend(continuing_codec_fns());
1640            }
1641            items
1642        },
1643    })
1644}
1645
1646/// `zerodds_dcps::dds_type::DdsType::{encode_xcdr1, decode_xcdr1,
1647/// encode_continuing, decode_continuing}` for a topic type.
1648///
1649/// DDS-RPC 1.0 §7.5.1.1: the Basic Service Mapping targets DDS 1.3, which
1650/// has classic CDR (XCDR1) only, where an 8-octet member aligns to 8 and
1651/// not to 4. The same inherent `encode`/`decode` write both — the writer's
1652/// alignment cap is the whole difference for a `@final` type — and the
1653/// `continuing` pair starts them at the offset the type has inside the
1654/// sample, behind the `RequestHeader` or `ReplyHeader`, so the alignment is
1655/// the sample's.
1656fn continuing_codec_fns() -> Vec<rust::Item> {
1657    fn path(parts: &[&str]) -> rust::Expr {
1658        rust::Expr::Path(Path::new(parts))
1659    }
1660    fn call(parts: &[&str], args: Vec<rust::Expr>) -> rust::Expr {
1661        rust::Expr::Call {
1662            callee: Box::new(path(parts)),
1663            args,
1664        }
1665    }
1666    fn local(name: &str) -> rust::Expr {
1667        rust::Expr::Path(Path::single(name))
1668    }
1669    fn mut_ref(name: &str) -> rust::Expr {
1670        rust::Expr::Ref {
1671            target: Box::new(local(name)),
1672            mutable: true,
1673        }
1674    }
1675    fn let_(name: &str, value: rust::Expr, mutable: bool) -> rust::Statement {
1676        rust::Statement::Let {
1677            name: Ident::new(name),
1678            type_: None,
1679            value: Some(value),
1680            mutable,
1681        }
1682    }
1683    fn method(receiver: rust::Expr, name: &str, args: Vec<rust::Expr>) -> rust::Expr {
1684        rust::Expr::MethodCall {
1685            receiver: Box::new(receiver),
1686            method: Ident::new(name),
1687            args,
1688        }
1689    }
1690    fn lit(n: i128) -> rust::Expr {
1691        rust::Expr::Literal(Literal::int(n))
1692    }
1693    let little = || path(&["zerodds_wire", "Endianness", "Little"]);
1694    let self_param = rust::Param {
1695        name: None,
1696        type_: rust::Type::unit(),
1697        receiver: Some(rust::Receiver::Ref),
1698    };
1699    let bytes_param = |name: &str| rust::Param {
1700        name: Some(Ident::new(name)),
1701        type_: rust::Type::Ref {
1702            target: Box::new(rust::Type::Slice(Box::new(rust::Type::Primitive(
1703                rust::Primitive::U8,
1704            )))),
1705            mutable: false,
1706            lifetime: None,
1707        },
1708        receiver: None,
1709    };
1710    let vec_u8_mut = rust::Type::Ref {
1711        target: Box::new(rust::Type::generic(
1712            Path::single("Vec"),
1713            vec![rust::Type::Primitive(rust::Primitive::U8)],
1714        )),
1715        mutable: true,
1716        lifetime: None,
1717    };
1718    let bool_param = |name: &str| rust::Param {
1719        name: Some(Ident::new(name)),
1720        type_: rust::Type::Primitive(rust::Primitive::Bool),
1721        receiver: None,
1722    };
1723    let encode_result = || {
1724        rust::Type::generic(
1725            Path::single("Result"),
1726            vec![
1727                rust::Type::unit(),
1728                rust::Type::named(Path::new(&["zerodds_dcps", "dds_type", "EncodeError"])),
1729            ],
1730        )
1731    };
1732    let decode_result = || {
1733        rust::Type::generic(
1734            Path::single("Result"),
1735            vec![
1736                rust::Type::named(Path::single("Self")),
1737                rust::Type::named(Path::new(&["zerodds_dcps", "dds_type", "DecodeError"])),
1738            ],
1739        )
1740    };
1741    let function = |name: &str,
1742                    params: Vec<rust::Param>,
1743                    returns: rust::Type,
1744                    statements: Vec<rust::Statement>| {
1745        rust::Item::Function(rust::Function {
1746            doc: Doc::none(),
1747            attrs: Vec::new(),
1748            visibility: Visibility::Private,
1749            name: Ident::new(name),
1750            generics: Vec::new(),
1751            params,
1752            returns: Some(returns),
1753            body: Some(rust::Block { statements }),
1754        })
1755    };
1756    // `if xcdr1 { 8 } else { 4 }`: the alignment cap of the stream.
1757    let cap = || rust::Expr::If {
1758        condition: Box::new(local("xcdr1")),
1759        then: rust::Block {
1760            statements: vec![rust::Statement::Tail(lit(8))],
1761        },
1762        otherwise: Some(rust::Block {
1763            statements: vec![rust::Statement::Tail(lit(4))],
1764        }),
1765    };
1766
1767    // The inherent pair of the version: `encode_xcdr1` for classic CDR,
1768    // `encode` for XCDR2.
1769    let by_version = |xcdr1_fn: &str, xcdr2_fn: &str, args: Vec<rust::Expr>| rust::Expr::If {
1770        condition: Box::new(local("xcdr1")),
1771        then: rust::Block {
1772            statements: vec![rust::Statement::Tail(call(
1773                &["Self", xcdr1_fn],
1774                args.clone(),
1775            ))],
1776        },
1777        otherwise: Some(rust::Block {
1778            statements: vec![rust::Statement::Tail(call(&["Self", xcdr2_fn], args))],
1779        }),
1780    };
1781
1782    // encode_xcdr1
1783    let encode_xcdr1 = function(
1784        "encode_xcdr1",
1785        vec![
1786            self_param.clone(),
1787            rust::Param {
1788                name: Some(Ident::new("out")),
1789                type_: vec_u8_mut.clone(),
1790                receiver: None,
1791            },
1792        ],
1793        encode_result(),
1794        vec![
1795            let_(
1796                "w",
1797                call(
1798                    &["zerodds_wire", "Writer", "with_max_align"],
1799                    vec![little(), lit(8)],
1800                ),
1801                true,
1802            ),
1803            rust::Statement::Semi(rust::Expr::Try(Box::new(call(
1804                &["Self", "encode_xcdr1"],
1805                vec![local("self"), mut_ref("w")],
1806            )))),
1807            rust::Statement::Semi(method(
1808                local("out"),
1809                "extend_from_slice",
1810                vec![rust::Expr::Ref {
1811                    target: Box::new(method(local("w"), "finish", Vec::new())),
1812                    mutable: false,
1813                }],
1814            )),
1815            rust::Statement::Tail(call(&["Ok"], vec![rust::Expr::Path(Path::single("()"))])),
1816        ],
1817    );
1818
1819    // decode_xcdr1
1820    let decode_xcdr1 = function(
1821        "decode_xcdr1",
1822        vec![bytes_param("bytes")],
1823        decode_result(),
1824        vec![
1825            let_(
1826                "r",
1827                call(
1828                    &["zerodds_wire", "Reader", "with_max_align"],
1829                    vec![local("bytes"), little(), lit(8)],
1830                ),
1831                true,
1832            ),
1833            rust::Statement::Tail(call(
1834                &["Ok"],
1835                vec![rust::Expr::Try(Box::new(call(
1836                    &["Self", "decode_xcdr1"],
1837                    vec![mut_ref("r")],
1838                )))],
1839            )),
1840        ],
1841    );
1842
1843    // encode_continuing
1844    let encode_continuing = function(
1845        "encode_continuing",
1846        vec![
1847            self_param,
1848            rust::Param {
1849                name: Some(Ident::new("prefix")),
1850                type_: vec_u8_mut,
1851                receiver: None,
1852            },
1853            bool_param("xcdr1"),
1854        ],
1855        encode_result(),
1856        vec![
1857            let_(
1858                "w",
1859                call(
1860                    &["zerodds_wire", "Writer", "continuing"],
1861                    vec![
1862                        little(),
1863                        cap(),
1864                        call(&["core", "mem", "take"], vec![local("prefix")]),
1865                    ],
1866                ),
1867                true,
1868            ),
1869            rust::Statement::Semi(rust::Expr::Try(Box::new(by_version(
1870                "encode_xcdr1",
1871                "encode",
1872                vec![local("self"), mut_ref("w")],
1873            )))),
1874            rust::Statement::Semi(rust::Expr::Assign {
1875                target: Box::new(rust::Expr::Deref(Box::new(local("prefix")))),
1876                value: Box::new(method(local("w"), "finish", Vec::new())),
1877            }),
1878            rust::Statement::Tail(call(&["Ok"], vec![rust::Expr::Path(Path::single("()"))])),
1879        ],
1880    );
1881
1882    // decode_continuing
1883    let decode_continuing = function(
1884        "decode_continuing",
1885        vec![
1886            bytes_param("bytes"),
1887            rust::Param {
1888                name: Some(Ident::new("offset")),
1889                type_: rust::Type::named(Path::single("usize")),
1890                receiver: None,
1891            },
1892            bool_param("xcdr1"),
1893        ],
1894        decode_result(),
1895        vec![
1896            let_(
1897                "r",
1898                call(
1899                    &["zerodds_wire", "Reader", "with_max_align"],
1900                    vec![local("bytes"), little(), cap()],
1901                ),
1902                true,
1903            ),
1904            rust::Statement::Semi(rust::Expr::Try(Box::new(method(
1905                local("r"),
1906                "skip_to",
1907                vec![local("offset")],
1908            )))),
1909            rust::Statement::Tail(call(
1910                &["Ok"],
1911                vec![rust::Expr::Try(Box::new(by_version(
1912                    "decode_xcdr1",
1913                    "decode",
1914                    vec![mut_ref("r")],
1915                )))],
1916            )),
1917        ],
1918    );
1919
1920    vec![
1921        encode_xcdr1,
1922        decode_xcdr1,
1923        encode_continuing,
1924        decode_continuing,
1925    ]
1926}
1927
1928// == the error type ========================================================
1929
1930fn error_enum(
1931    ast: &Ast,
1932    module: Id<ModuleNode>,
1933    service: &Service,
1934    exceptions: &[Raise],
1935) -> Result<rust::Enum, RustInterfaceError> {
1936    let mut variants = Vec::new();
1937    for raise in exceptions {
1938        variants.push(rust::Variant {
1939            doc: Doc::line(&format!(
1940                "The declared `{}` exception. DDS-RPC 1.0 §7.5.1.1.5.",
1941                raise.name
1942            )),
1943            attrs: Vec::new(),
1944            name: Ident::new(&rust::escape_keyword(&raise.name)),
1945            body: rust::VariantBody::Tuple(vec![exception_type(ast, module, raise.id)?]),
1946            discriminant: None,
1947        });
1948    }
1949    variants.push(rust::Variant {
1950        doc: Doc::lines(&[
1951            "A DDS-RPC 1.0 §7.11.1.3 remote/system exception, or a transport",
1952            "failure `zerodds_rpc::requester::Requester` reports — a timeout,",
1953            "an encode/decode failure, or a DCPS error.",
1954        ]),
1955        attrs: Vec::new(),
1956        name: Ident::new("Remote"),
1957        body: rust::VariantBody::Tuple(vec![rust::Type::named(Path::new(&[
1958            "zerodds_rpc",
1959            "error",
1960            "RpcError",
1961        ]))]),
1962        discriminant: None,
1963    });
1964    Ok(rust::Enum {
1965        doc: Doc::line(&format!("Every way a call to `{}` can fail.", service.name)),
1966        attrs: Vec::new(),
1967        derives: data_derives(),
1968        visibility: Visibility::Public,
1969        name: Ident::new(&error_type_name(service)),
1970        generics: Vec::new(),
1971        variants,
1972    })
1973}
1974
1975// == the operation surface: trait, client, dispatcher =====================
1976
1977/// One of the three things a `Result<T, {svc}Error>` value a client
1978/// method or a service trait method carries besides its error: the
1979/// operation's own return value, or one pure `out` parameter (never
1980/// `inout`, which travels through a `&mut` argument instead and is
1981/// never part of the `Ok` value).
1982#[derive(Clone, PartialEq, Eq)]
1983enum OutFieldSource {
1984    Return,
1985    Out(String),
1986    InOut(String),
1987}
1988
1989fn same_source(a: &OutFieldSource, b: &OutFieldSource) -> bool {
1990    matches!((a, b), (OutFieldSource::Return, OutFieldSource::Return))
1991        || matches!((a, b), (OutFieldSource::Out(x), OutFieldSource::Out(y)) if x == y)
1992}
1993
1994/// The `${I}_${op}_Out` structure's own fields, each paired with what
1995/// it is. The same order [`out_fields`] declares the structure in.
1996fn out_field_map(op: &Operation) -> Vec<(String, OutFieldSource)> {
1997    let mut out = Vec::new();
1998    for p in op.outputs() {
1999        let field_name = rust::escape_keyword(&p.name);
2000        let source = match p.direction {
2001            ParamDirection::InOut => OutFieldSource::InOut(p.name.clone()),
2002            _ => OutFieldSource::Out(p.name.clone()),
2003        };
2004        out.push((field_name, source));
2005    }
2006    if op.returns.is_some() {
2007        let taken: Vec<&str> = out.iter().map(|(name, _)| name.as_str()).collect();
2008        out.push((return_member_name(&taken), OutFieldSource::Return));
2009    }
2010    out
2011}
2012
2013/// The positions the method's own `Ok` value carries: the return first,
2014/// when the operation has one, then every pure `out` parameter in
2015/// declaration order. Index 0 is tuple index `.0`, and so on — the same
2016/// order the client and the dispatcher both read, so the two sides
2017/// agree on which slot is which without a third reading of the
2018/// operation.
2019fn ok_positions(op: &Operation) -> Vec<OutFieldSource> {
2020    let mut out = Vec::new();
2021    if op.returns.is_some() {
2022        out.push(OutFieldSource::Return);
2023    }
2024    for p in op
2025        .params
2026        .iter()
2027        .filter(|p| matches!(p.direction, ParamDirection::Out))
2028    {
2029        out.push(OutFieldSource::Out(p.name.clone()));
2030    }
2031    out
2032}
2033
2034/// Where `target` sits in the `Ok` value [`ok_positions`] lays out.
2035///
2036/// Both lists come from the same operation, so a miss is this module
2037/// contradicting itself — and answering position 0 for it would bind a
2038/// generated field to another field's value without a word.
2039fn position_index(
2040    positions: &[OutFieldSource],
2041    target: &OutFieldSource,
2042    op: &Operation,
2043) -> Result<usize, RustInterfaceError> {
2044    positions
2045        .iter()
2046        .position(|p| same_source(p, target))
2047        .ok_or_else(|| {
2048            RustInterfaceError::Interface(InterfaceError::Unresolved {
2049                construct: "out value's position in the Ok value",
2050                at: op.name.clone(),
2051            })
2052        })
2053}
2054
2055/// The expression that reads `source` out of `binding`, a value shaped
2056/// by [`ok_type`]/[`ok_positions`] for this operation.
2057fn ok_component_expr(
2058    positions: &[OutFieldSource],
2059    source: &OutFieldSource,
2060    binding: &str,
2061    op: &Operation,
2062) -> Result<rust::Expr, RustInterfaceError> {
2063    if positions.len() <= 1 {
2064        Ok(rust::Expr::Path(Path::single(binding)))
2065    } else {
2066        let index = position_index(positions, source, op)?;
2067        Ok(field_expr(binding, &index.to_string()))
2068    }
2069}
2070
2071fn ok_type(
2072    ast: &Ast,
2073    module: Id<ModuleNode>,
2074    op: &Operation,
2075) -> Result<rust::Type, RustInterfaceError> {
2076    let mut parts = Vec::new();
2077    if let Some(target) = op.returns {
2078        parts.push(rust_type_of(ast, module, Some(target))?);
2079    }
2080    for p in op
2081        .params
2082        .iter()
2083        .filter(|p| matches!(p.direction, ParamDirection::Out))
2084    {
2085        parts.push(rust_type_of(ast, module, Some(p.type_))?);
2086    }
2087    Ok(match parts.len() {
2088        0 => rust::Type::unit(),
2089        1 => parts.remove(0),
2090        _ => rust::Type::Tuple(parts),
2091    })
2092}
2093
2094fn op_return_type(
2095    ast: &Ast,
2096    module: Id<ModuleNode>,
2097    service: &Service,
2098    op: &Operation,
2099) -> Result<rust::Type, RustInterfaceError> {
2100    Ok(rust::Type::generic(
2101        Path::single("Result"),
2102        vec![
2103            ok_type(ast, module, op)?,
2104            rust::Type::named(Path::single(&error_type_name(service))),
2105        ],
2106    ))
2107}
2108
2109/// The parameters a service trait method, and a client method, take.
2110///
2111/// `in` by value, `inout` as `&mut T` — it has an initial value to
2112/// read — and a pure `out` is not a parameter at all: it has none to
2113/// read, and travels back through the `Ok` value instead
2114/// ([`ok_type`]), which is what lets a Rust caller call the method
2115/// without first inventing one.
2116fn op_params(
2117    ast: &Ast,
2118    module: Id<ModuleNode>,
2119    op: &Operation,
2120) -> Result<Vec<rust::Param>, RustInterfaceError> {
2121    let mut params = vec![rust::Param {
2122        name: None,
2123        type_: rust::Type::unit(),
2124        receiver: Some(rust::Receiver::Ref),
2125    }];
2126    for p in &op.params {
2127        let base = rust_type_of(ast, module, Some(p.type_))?;
2128        match p.direction {
2129            ParamDirection::In => params.push(rust::Param {
2130                name: Some(Ident::new(&rust::escape_keyword(&p.name))),
2131                type_: base,
2132                receiver: None,
2133            }),
2134            ParamDirection::InOut => params.push(rust::Param {
2135                name: Some(Ident::new(&rust::escape_keyword(&p.name))),
2136                type_: rust::Type::Ref {
2137                    target: Box::new(base),
2138                    mutable: true,
2139                    lifetime: None,
2140                },
2141                receiver: None,
2142            }),
2143            ParamDirection::Out => {}
2144        }
2145    }
2146    Ok(params)
2147}
2148
2149/// A one-line doc for a generated operation: its `raises` clause where it
2150/// has one (DDS-RPC 1.0 §7.11.1.1.2 rule 6.c keeps the clause out of the
2151/// signature itself, so the doc line is where a reader finds it), else a
2152/// plain statement of what the IDL declared — every `pub` item this module
2153/// emits carries a doc, per the workspace's `missing_docs` lint.
2154fn method_doc(op: &Operation) -> String {
2155    op.raises_line().unwrap_or_else(|| {
2156        format!(
2157            "Generated from the IDL operation `{}`; raises nothing.",
2158            op.name
2159        )
2160    })
2161}
2162
2163fn trait_method_signature(
2164    ast: &Ast,
2165    service: &Service,
2166    op: &Operation,
2167) -> Result<rust::Function, RustInterfaceError> {
2168    Ok(rust::Function {
2169        doc: Doc::line(&method_doc(op)),
2170        attrs: Vec::new(),
2171        visibility: Visibility::Private,
2172        name: Ident::new(&rust::escape_keyword(&op.name)),
2173        generics: Vec::new(),
2174        params: op_params(ast, service.module, op)?,
2175        returns: Some(op_return_type(ast, service.module, service, op)?),
2176        body: None,
2177    })
2178}
2179
2180fn base_interface_path(
2181    ast: &Ast,
2182    from: Id<ModuleNode>,
2183    base: &interface::Base,
2184    suffix: &str,
2185) -> Result<Path, RustInterfaceError> {
2186    let node = ast.interface(base.id).map_err(|_| InterfaceError::NoNode {
2187        kind: "base interface",
2188        at: base.name.clone(),
2189    })?;
2190    relative_module_path(ast, from, node.module, &format!("{}{suffix}", node.name))
2191}
2192
2193fn service_trait(ast: &Ast, service: &Service) -> Result<rust::Trait, RustInterfaceError> {
2194    let mut supertraits = Vec::new();
2195    for base in &service.bases {
2196        supertraits.push(rust::Type::named(base_interface_path(
2197            ast,
2198            service.module,
2199            base,
2200            "",
2201        )?));
2202    }
2203    let mut items = Vec::new();
2204    for op in service.all_operations() {
2205        items.push(rust::Item::Function(trait_method_signature(
2206            ast, service, &op,
2207        )?));
2208    }
2209    let mut doc = Doc::line(&format!(
2210        "The `{}` service. DDS-RPC 1.0 §7.5.1.1.3, §7.11.",
2211        service.name
2212    ));
2213    doc.push(
2214        "`out`/`inout` parameters are `&mut` arguments; a pure `out` parameter \
2215         besides the declared return travels back through the `Ok` value instead \
2216         — `()` for none, the bare value for one, a tuple (return first, then \
2217         every `out` in declaration order) for more than one.",
2218    );
2219    Ok(rust::Trait {
2220        doc,
2221        attrs: Vec::new(),
2222        visibility: Visibility::Public,
2223        name: Ident::new(&rust::escape_keyword(&service.name)),
2224        generics: Vec::new(),
2225        supertraits,
2226        items,
2227    })
2228}
2229
2230// -- the client -------------------------------------------------------------
2231
2232fn client_type_name(service: &Service) -> String {
2233    format!("{}Client", service.name)
2234}
2235
2236fn client_field_name(base: &interface::Base) -> String {
2237    format!("base_{}", base.name.to_lowercase())
2238}
2239
2240fn client_struct(ast: &Ast, service: &Service) -> Result<rust::Struct, RustInterfaceError> {
2241    let mut fields = vec![field_vis(
2242        "requester",
2243        rust::Type::generic(
2244            Path::new(&["zerodds_rpc", "requester", "Requester"]),
2245            vec![
2246                rust::Type::named(Path::single(&interface::call_union_name(&service.name))),
2247                rust::Type::named(Path::single(&interface::return_union_name(&service.name))),
2248            ],
2249        ),
2250        Visibility::Private,
2251        "The `Requester` this interface's own operations send `${I}_Call`s \
2252         through and read `${I}_Return`s back from — DDS-RPC 1.0 §7.9.",
2253    )];
2254    for base in &service.bases {
2255        let path = base_interface_path(ast, service.module, base, "Client")?;
2256        fields.push(field_vis(
2257            &client_field_name(base),
2258            rust::Type::named(path),
2259            Visibility::Public,
2260            &format!(
2261                "The `{}` client this interface's own base composes — DDS-RPC 1.0 \
2262                 §7.5.1.1.8 keeps its topic separate from this one's.",
2263                base.name
2264            ),
2265        ));
2266    }
2267    let mut doc = Doc::line("A typed DDS-RPC client. DDS-RPC 1.0 §7.11.1.5.4/§7.9.");
2268    doc.push(
2269        "A base interface's operations are not repeated here — DDS-RPC 1.0 keeps a \
2270         base's topic separate from a derived interface's own (§7.5.1.1.8) — they \
2271         are reachable through the base client field this struct composes, one per \
2272         parent interface, so a call to an inherited operation is one field access \
2273         away rather than absent.",
2274    );
2275    Ok(rust::Struct {
2276        body_text: zerodds_lang_cst::common::BodyText::none(),
2277        doc,
2278        attrs: Vec::new(),
2279        derives: Vec::new(),
2280        visibility: Visibility::Public,
2281        name: Ident::new(&client_type_name(service)),
2282        generics: Vec::new(),
2283        body: rust::StructBody::Named(fields),
2284    })
2285}
2286
2287fn dcps_participant_type() -> rust::Type {
2288    rust::Type::Ref {
2289        target: Box::new(rust::Type::named(Path::new(&[
2290            "zerodds_dcps",
2291            "participant",
2292            "DomainParticipant",
2293        ]))),
2294        mutable: false,
2295        lifetime: None,
2296    }
2297}
2298
2299fn rpc_qos_type() -> rust::Type {
2300    rust::Type::Ref {
2301        target: Box::new(rust::Type::named(Path::new(&[
2302            "zerodds_rpc",
2303            "qos_profile",
2304            "RpcQos",
2305        ]))),
2306        mutable: false,
2307        lifetime: None,
2308    }
2309}
2310
2311fn rpc_result_of(ok: rust::Type) -> rust::Type {
2312    rust::Type::generic(Path::new(&["zerodds_rpc", "error", "RpcResult"]), vec![ok])
2313}
2314
2315fn try_call(callee: Path, args: Vec<rust::Expr>) -> rust::Expr {
2316    rust::Expr::Try(Box::new(rust::Expr::Call {
2317        callee: Box::new(rust::Expr::Path(callee)),
2318        args,
2319    }))
2320}
2321
2322/// `"text".to_string()` — an owned `String` from a literal, for a field
2323/// whose type is `String`/`Option<String>` rather than `&str`.
2324fn owned_string_literal(text: &str) -> rust::Expr {
2325    rust::Expr::MethodCall {
2326        receiver: Box::new(rust::Expr::Literal(Literal::string(text))),
2327        method: Ident::new("to_string"),
2328        args: Vec::new(),
2329    }
2330}
2331
2332/// `Some("text".to_string())` / `None::<String>` for an `Option<String>`
2333/// field, from an `Option<String>` already read off the model.
2334fn optional_owned_string(value: Option<&str>) -> rust::Expr {
2335    match value {
2336        Some(text) => rust::Expr::Call {
2337            callee: Box::new(rust::Expr::Path(Path::single("Some"))),
2338            args: vec![owned_string_literal(text)],
2339        },
2340        None => rust::Expr::Path(Path::new(&["None"])),
2341    }
2342}
2343
2344/// `&zerodds_rpc::topic_naming::AnnotatedTopics { .. }` for `service` —
2345/// its own `@service`/`@DDSRequestTopic`/`@DDSReplyTopic` values
2346/// (`adapter::interface::Service::service_name`/`request_topic`/
2347/// `reply_topic`, already read off the IDL's facets), no run-time
2348/// override (`TopicNameOverrides::default()` — a caller with one does
2349/// not go through the generated `new`/`serve` at all). DDS-RPC 1.0
2350/// §7.9/§7.10 give the `Requester`/`Replier` exactly these three
2351/// service-declared inputs; this is the generated binding reading them
2352/// instead of leaving every service at the `Calculator_Service_Request`
2353/// default regardless of what its own IDL declared.
2354fn annotated_topics_expr(service: &Service) -> rust::Expr {
2355    rust::Expr::Ref {
2356        target: Box::new(rust::Expr::StructLit {
2357            path: Path::new(&["zerodds_rpc", "topic_naming", "AnnotatedTopics"]),
2358            fields: vec![
2359                (
2360                    Ident::new("service_name"),
2361                    optional_owned_string(Some(service.service_name.as_str())),
2362                ),
2363                (
2364                    Ident::new("request_topic"),
2365                    optional_owned_string(service.request_topic.as_deref()),
2366                ),
2367                (
2368                    Ident::new("reply_topic"),
2369                    optional_owned_string(service.reply_topic.as_deref()),
2370                ),
2371                (
2372                    Ident::new("overrides"),
2373                    rust::Expr::Call {
2374                        callee: Box::new(rust::Expr::Path(Path::new(&[
2375                            "zerodds_rpc",
2376                            "topic_naming",
2377                            "TopicNameOverrides",
2378                            "none",
2379                        ]))),
2380                        args: Vec::new(),
2381                    },
2382                ),
2383            ],
2384            base: None,
2385        }),
2386        mutable: false,
2387    }
2388}
2389
2390/// `&zerodds_rpc::endpoint_options::RpcEndpointOptions::for_interface(scope,
2391/// name).with_parent(..)` for `service`: the DDS type names of its topics
2392/// — `${interfaceName}_Request` and `_Reply` in the module the interface
2393/// is declared in (§7.5.1.1.6, §7.5.1.1.7) — and the base interfaces whose
2394/// topics are its aliases (§7.4.3).
2395fn endpoint_options_expr(ast: &Ast, service: &Service) -> Result<rust::Expr, RustInterfaceError> {
2396    Ok(rust::Expr::Ref {
2397        target: Box::new(endpoint_options_value(ast, service)?),
2398        mutable: false,
2399    })
2400}
2401
2402/// [`endpoint_options_expr`] without the reference: the options by value.
2403fn endpoint_options_value(ast: &Ast, service: &Service) -> Result<rust::Expr, RustInterfaceError> {
2404    let scope = interface::module_chain(ast, service.module)?
2405        .into_iter()
2406        .map(|(_, name)| name)
2407        .collect::<Vec<_>>()
2408        .join("::");
2409    let mut options = rust::Expr::Call {
2410        callee: Box::new(rust::Expr::Path(Path::new(&[
2411            "zerodds_rpc",
2412            "endpoint_options",
2413            "RpcEndpointOptions",
2414            "for_interface",
2415        ]))),
2416        args: vec![
2417            rust::Expr::Literal(Literal::string(&scope)),
2418            rust::Expr::Literal(Literal::string(&service.name)),
2419        ],
2420    };
2421    for base in &service.bases {
2422        options = rust::Expr::MethodCall {
2423            receiver: Box::new(options),
2424            method: Ident::new("with_parent"),
2425            args: vec![rust::Expr::Literal(Literal::string(&base.qualified_name))],
2426        };
2427    }
2428    if service.mapping.is_enhanced() {
2429        options = rust::Expr::MethodCall {
2430            receiver: Box::new(options),
2431            method: Ident::new("with_mapping"),
2432            args: vec![rust::Expr::Path(Path::new(&[
2433                "zerodds_rpc",
2434                "endpoint_options",
2435                "RpcMapping",
2436                "Enhanced",
2437            ]))],
2438        };
2439        // The XTypes description of the two topic types, when every type
2440        // they name has an object. A recursive type, or one XTypes 1.3 §7.3
2441        // has no identifier for, leaves the topics matched by type name.
2442        let model = interface::enhanced_service(ast, service, service.mapping.empty_style())?;
2443        if let Ok(objects) = type_information::service_type_objects(ast, service, &model) {
2444            options = rust::Expr::MethodCall {
2445                receiver: Box::new(options),
2446                method: Ident::new("with_type_information"),
2447                args: vec![type_information_expr(&objects)],
2448            };
2449        }
2450    }
2451    Ok(options)
2452}
2453
2454/// `zerodds_rpc::endpoint_options::RpcTypeInformation { request, reply }`
2455/// over the bytes of `objects`.
2456fn type_information_expr(objects: &type_information::ServiceTypeObjects) -> rust::Expr {
2457    let topic = |topic: &type_information::TopicTypeObjects| rust::Expr::StructLit {
2458        path: Path::new(&["zerodds_rpc", "endpoint_options", "TopicTypeBytes"]),
2459        fields: vec![
2460            (
2461                Ident::new("object"),
2462                rust::Expr::Literal(Literal::Bytes(topic.object.clone())),
2463            ),
2464            (
2465                Ident::new("dependencies"),
2466                rust::Expr::Ref {
2467                    target: Box::new(rust::Expr::Array(
2468                        topic
2469                            .dependencies
2470                            .iter()
2471                            .map(|bytes| rust::Expr::Literal(Literal::Bytes(bytes.clone())))
2472                            .collect(),
2473                    )),
2474                    mutable: false,
2475                },
2476            ),
2477        ],
2478        base: None,
2479    };
2480    rust::Expr::StructLit {
2481        path: Path::new(&["zerodds_rpc", "endpoint_options", "RpcTypeInformation"]),
2482        fields: vec![
2483            (Ident::new("request"), topic(&objects.request)),
2484            (Ident::new("reply"), topic(&objects.reply)),
2485        ],
2486        base: None,
2487    }
2488}
2489
2490fn binding_style_expr() -> rust::Expr {
2491    rust::Expr::Path(Path::new(&[
2492        "zerodds_rpc",
2493        "topic_naming",
2494        "BindingStyle",
2495        "FunctionCall",
2496    ]))
2497}
2498
2499fn client_impl(ast: &Ast, service: &Service) -> Result<rust::Item, RustInterfaceError> {
2500    let mut new_fields = vec![(
2501        Ident::new("requester"),
2502        try_call(
2503            Path::new(&["zerodds_rpc", "requester", "Requester", "with_options"]),
2504            vec![
2505                rust::Expr::Path(Path::single("participant")),
2506                rust::Expr::Literal(Literal::string(&service.qualified_name)),
2507                rust::Expr::Literal(Literal::string("")),
2508                rust::Expr::Path(Path::single("qos")),
2509                binding_style_expr(),
2510                annotated_topics_expr(service),
2511                endpoint_options_expr(ast, service)?,
2512            ],
2513        ),
2514    )];
2515    for base in &service.bases {
2516        let path = base_interface_path(ast, service.module, base, "Client")?;
2517        new_fields.push((
2518            Ident::new(&client_field_name(base)),
2519            try_call(
2520                path.join("new"),
2521                vec![
2522                    rust::Expr::Path(Path::single("participant")),
2523                    rust::Expr::Path(Path::single("qos")),
2524                ],
2525            ),
2526        ));
2527    }
2528    let new_fn = rust::Item::Function(rust::Function {
2529        doc: Doc::line(
2530            "A client bound to a `DomainParticipant`, one `Requester` per interface level.",
2531        ),
2532        attrs: Vec::new(),
2533        visibility: Visibility::Public,
2534        name: Ident::new("new"),
2535        generics: Vec::new(),
2536        params: vec![
2537            rust::Param {
2538                name: Some(Ident::new("participant")),
2539                type_: dcps_participant_type(),
2540                receiver: None,
2541            },
2542            rust::Param {
2543                name: Some(Ident::new("qos")),
2544                type_: rpc_qos_type(),
2545                receiver: None,
2546            },
2547        ],
2548        returns: Some(rpc_result_of(rust::Type::named(Path::single("Self")))),
2549        body: Some(rust::Block {
2550            statements: vec![rust::Statement::Tail(rust::Expr::Call {
2551                callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2552                args: vec![rust::Expr::StructLit {
2553                    path: Path::single("Self"),
2554                    fields: new_fields,
2555                    base: None,
2556                }],
2557            })],
2558        }),
2559    });
2560
2561    let options_fn = rust::Item::Function(rust::Function {
2562        doc: Doc::line(
2563            "The options this interface's endpoints are created with: the DDS type names of \
2564             the topics, the base interfaces, the service mapping and, under the Enhanced \
2565             mapping, the TypeObjects of the topic types. An endpoint written by hand for \
2566             the same topics, on a participant that also holds this binding, has to use \
2567             them: a topic name is reused only with the same type metadata.",
2568        ),
2569        attrs: Vec::new(),
2570        visibility: Visibility::Public,
2571        name: Ident::new("endpoint_options"),
2572        generics: Vec::new(),
2573        params: Vec::new(),
2574        returns: Some(rust::Type::named(Path::new(&[
2575            "zerodds_rpc",
2576            "endpoint_options",
2577            "RpcEndpointOptions",
2578        ]))),
2579        body: Some(rust::Block {
2580            statements: vec![rust::Statement::Tail(endpoint_options_value(ast, service)?)],
2581        }),
2582    });
2583    let mut items = vec![new_fn, options_fn];
2584    if let Some(request_profile) = service.qos_request_profile.as_deref() {
2585        items.push(declared_qos_fn(
2586            "new_with_declared_qos",
2587            "A client whose QoS is the one `@Qos` declares for this interface (DDS-RPC 1.0 §7.10.1).",
2588            request_profile,
2589            service.qos_reply_profile.as_deref(),
2590            None,
2591            rpc_result_of(rust::Type::named(Path::single("Self"))),
2592            &["Self", "new"],
2593        ));
2594    }
2595    for op in service.all_operations() {
2596        items.push(rust::Item::Function(client_method(ast, service, &op)?));
2597    }
2598
2599    Ok(rust::Item::Impl(rust::Impl {
2600        doc: Doc::none(),
2601        attrs: Vec::new(),
2602        generics: Vec::new(),
2603        trait_: None,
2604        self_type: rust::Type::named(Path::single(&client_type_name(service))),
2605        items,
2606    }))
2607}
2608
2609/// A constructor whose QoS is `@Qos(RequestProfile, ReplyProfile)`
2610/// (§7.10.1): reads the profiles from their URLs and calls `delegate`
2611/// with the result. `receiver` is the `self` the delegate also takes.
2612fn declared_qos_fn(
2613    name: &str,
2614    doc: &str,
2615    request_profile: &str,
2616    reply_profile: Option<&str>,
2617    receiver: Option<rust::Receiver>,
2618    returns: rust::Type,
2619    delegate: &[&str],
2620) -> rust::Item {
2621    let reply = match reply_profile {
2622        Some(url) => rust::Expr::Call {
2623            callee: Box::new(rust::Expr::Path(Path::single("Some"))),
2624            args: vec![rust::Expr::Literal(Literal::string(url))],
2625        },
2626        None => rust::Expr::Path(Path::single("None")),
2627    };
2628    let load_qos = rust::Statement::Let {
2629        name: Ident::new("qos"),
2630        type_: None,
2631        value: Some(try_call(
2632            Path::new(&[
2633                "zerodds_rpc",
2634                "qos_profile",
2635                "RpcQos",
2636                "from_service_profiles",
2637            ]),
2638            vec![rust::Expr::Literal(Literal::string(request_profile)), reply],
2639        )),
2640        mutable: false,
2641    };
2642    let mut call_args = Vec::new();
2643    if receiver.is_some() {
2644        call_args.push(rust::Expr::Path(Path::single("self")));
2645    }
2646    call_args.push(rust::Expr::Path(Path::single("participant")));
2647    call_args.push(rust::Expr::Ref {
2648        target: Box::new(rust::Expr::Path(Path::single("qos"))),
2649        mutable: false,
2650    });
2651    let mut params = Vec::new();
2652    if let Some(receiver) = receiver {
2653        params.push(rust::Param {
2654            name: None,
2655            type_: rust::Type::unit(),
2656            receiver: Some(receiver),
2657        });
2658    }
2659    params.push(rust::Param {
2660        name: Some(Ident::new("participant")),
2661        type_: dcps_participant_type(),
2662        receiver: None,
2663    });
2664    rust::Item::Function(rust::Function {
2665        doc: Doc::line(doc),
2666        attrs: Vec::new(),
2667        visibility: Visibility::Public,
2668        name: Ident::new(name),
2669        generics: Vec::new(),
2670        params,
2671        returns: Some(returns),
2672        body: Some(rust::Block {
2673            statements: vec![
2674                load_qos,
2675                rust::Statement::Tail(rust::Expr::Call {
2676                    callee: Box::new(rust::Expr::Path(Path::new(delegate))),
2677                    args: call_args,
2678                }),
2679            ],
2680        }),
2681    })
2682}
2683
2684fn requester_field() -> rust::Expr {
2685    field_expr("self", "requester")
2686}
2687
2688fn map_err_remote(service: &Service, value: rust::Expr) -> rust::Expr {
2689    rust::Expr::MethodCall {
2690        receiver: Box::new(value),
2691        method: Ident::new("map_err"),
2692        args: vec![rust::Expr::Path(Path::new(&[
2693            &error_type_name(service),
2694            "Remote",
2695        ]))],
2696    }
2697}
2698
2699/// Whether `rustc` derives `Copy` for a [`rust::Type`] this generator
2700/// materialises — the scalar primitives DDS-RPC 1.0's own IDL primitive
2701/// mapping produces. Anything else (`String`, a generated `struct`, a
2702/// generated `enum`) is `Clone`-only.
2703fn is_copy_primitive(type_: &rust::Type) -> bool {
2704    matches!(
2705        type_,
2706        rust::Type::Primitive(
2707            rust::Primitive::Bool
2708                | rust::Primitive::Char
2709                | rust::Primitive::I8
2710                | rust::Primitive::I16
2711                | rust::Primitive::I32
2712                | rust::Primitive::I64
2713                | rust::Primitive::I128
2714                | rust::Primitive::U8
2715                | rust::Primitive::U16
2716                | rust::Primitive::U32
2717                | rust::Primitive::U64
2718                | rust::Primitive::U128
2719                | rust::Primitive::F32
2720                | rust::Primitive::F64
2721        )
2722    )
2723}
2724
2725fn in_struct_literal(
2726    ast: &Ast,
2727    module: Id<ModuleNode>,
2728    op: &Operation,
2729    path: Path,
2730    empty: interface::EmptyOperationStyle,
2731) -> Result<rust::Expr, RustInterfaceError> {
2732    let mut fields = Vec::new();
2733    for p in op.inputs() {
2734        let name = rust::escape_keyword(&p.name);
2735        // An `in` parameter (`op_params`) is already owned by this method,
2736        // so it moves straight into the payload. An `inout` one is
2737        // `&mut T` (`rust::Type::Ref { mutable: true, .. }`): the payload
2738        // needs an owned `T` and the caller keeps the reference to assign
2739        // the reply back into (`client_reply_match`), so it is read
2740        // through the reference — by value (`*x`) where `T` is one of
2741        // DDS-RPC 1.0's `Copy` primitives, else by `.clone()`, so neither
2742        // `clippy::clone_on_copy` nor a move out of a `&mut` reference.
2743        let value = if matches!(p.direction, ParamDirection::InOut) {
2744            let target = rust_type_of(ast, module, Some(p.type_))?;
2745            if is_copy_primitive(&target) {
2746                rust::Expr::Deref(Box::new(rust::Expr::Path(Path::single(&name))))
2747            } else {
2748                rust::Expr::MethodCall {
2749                    receiver: Box::new(rust::Expr::Path(Path::single(&name))),
2750                    method: Ident::new("clone"),
2751                    args: Vec::new(),
2752                }
2753            }
2754        } else {
2755            rust::Expr::Path(Path::single(&name))
2756        };
2757        fields.push((Ident::new(&name), value));
2758    }
2759    if fields.is_empty() && empty == interface::EmptyOperationStyle::DummyMember {
2760        fields.push((
2761            Ident::new(interface::DUMMY_MEMBER),
2762            rust::Expr::Literal(Literal::int(0)),
2763        ));
2764    }
2765    Ok(rust::Expr::StructLit {
2766        path,
2767        fields,
2768        base: None,
2769    })
2770}
2771
2772/// A client method's body, for one operation of `service`.
2773///
2774/// Send, block for the reply, and take the reply union's own
2775/// case apart into the operation's `Ok`/`Err`. A reply whose case does
2776/// not match the operation called is a protocol error and not
2777/// constructed by this binding's own dispatcher; it is reported through
2778/// `{svc}Error::Remote` rather than panicked on.
2779fn client_method(
2780    ast: &Ast,
2781    service: &Service,
2782    op: &Operation,
2783) -> Result<rust::Function, RustInterfaceError> {
2784    let module = service.module;
2785    let params = op_params(ast, module, op)?;
2786    let returns = op_return_type(ast, module, service, op)?;
2787    let variant = variant_name(&op.name);
2788    let in_struct_path = Path::single(&interface::in_struct_name(&service.name, &op.name));
2789    let call_path = Path::new(&[&interface::call_union_name(&service.name), &variant]);
2790
2791    let build_payload = rust::Statement::Let {
2792        name: Ident::new("request_payload"),
2793        type_: None,
2794        value: Some(in_struct_literal(
2795            ast,
2796            module,
2797            op,
2798            in_struct_path,
2799            service.mapping.empty_style(),
2800        )?),
2801        mutable: false,
2802    };
2803    let build_call = rust::Statement::Let {
2804        name: Ident::new("call"),
2805        type_: None,
2806        value: Some(rust::Expr::Call {
2807            callee: Box::new(rust::Expr::Path(call_path)),
2808            args: vec![rust::Expr::Path(Path::single("request_payload"))],
2809        }),
2810        mutable: false,
2811    };
2812
2813    let send = rust::Expr::MethodCall {
2814        receiver: Box::new(requester_field()),
2815        method: Ident::new("send_request_blocking"),
2816        args: vec![
2817            rust::Expr::Ref {
2818                target: Box::new(rust::Expr::Path(Path::single("call"))),
2819                mutable: false,
2820            },
2821            rust::Expr::Path(Path::single("None")),
2822        ],
2823    };
2824    let reply_let = rust::Statement::Let {
2825        name: Ident::new("reply"),
2826        type_: None,
2827        value: Some(rust::Expr::Try(Box::new(map_err_remote(service, send)))),
2828        mutable: false,
2829    };
2830    let statements = vec![
2831        build_payload,
2832        build_call,
2833        reply_let,
2834        client_reply_match(service, op, &variant)?,
2835    ];
2836
2837    Ok(rust::Function {
2838        doc: Doc::line(&method_doc(op)),
2839        attrs: Vec::new(),
2840        visibility: Visibility::Public,
2841        name: Ident::new(&rust::escape_keyword(&op.name)),
2842        generics: Vec::new(),
2843        params,
2844        returns: Some(returns),
2845        body: Some(rust::Block { statements }),
2846    })
2847}
2848
2849fn client_reply_match(
2850    service: &Service,
2851    op: &Operation,
2852    variant: &str,
2853) -> Result<rust::Statement, RustInterfaceError> {
2854    let map = out_field_map(op);
2855    let positions = ok_positions(op);
2856    let mut assigns = Vec::new();
2857    let mut ok_values = Vec::new();
2858    for (field_name, source) in &map {
2859        match source {
2860            OutFieldSource::InOut(param_name) => {
2861                assigns.push(rust::Statement::Semi(rust::Expr::Assign {
2862                    target: Box::new(rust::Expr::Deref(Box::new(rust::Expr::Path(Path::single(
2863                        &rust::escape_keyword(param_name),
2864                    ))))),
2865                    value: Box::new(field_expr("out_val", field_name)),
2866                }));
2867            }
2868            OutFieldSource::Return | OutFieldSource::Out(_) => {
2869                let index = position_index(&positions, source, op)?;
2870                ok_values.push((index, field_expr("out_val", field_name)));
2871            }
2872        }
2873    }
2874    ok_values.sort_by_key(|(index, _)| *index);
2875    let ok_expr = match ok_values.len() {
2876        0 => ok_unit(),
2877        1 => rust::Expr::Call {
2878            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2879            args: vec![ok_values.remove(0).1],
2880        },
2881        _ => rust::Expr::Call {
2882            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2883            args: vec![rust::Expr::Tuple(
2884                ok_values.into_iter().map(|(_, expr)| expr).collect(),
2885            )],
2886        },
2887    };
2888    let mut ok_arm_statements = assigns;
2889    ok_arm_statements.push(rust::Statement::Tail(ok_expr));
2890
2891    let result_name = interface::result_union_name(&service.name, &op.name);
2892    // `out_val` is read by `assigns`/`ok_values` above only where the
2893    // reply carries an `out`, `inout`, or return value (DDS-RPC 1.0
2894    // §7.5.1.1.5); an operation whose `${I}_${op}_Out` is empty (no
2895    // outputs, `void` return) binds a wildcard instead so `rustc` does
2896    // not warn on the unread payload.
2897    let ok_binding = if map.is_empty() {
2898        rust::Pattern::Wildcard
2899    } else {
2900        rust::Pattern::Binding(Ident::new("out_val"))
2901    };
2902    let mut result_arms = vec![rust::MatchArm {
2903        pattern: rust::Pattern::TupleStruct {
2904            path: Path::new(&[&result_name, "Ok"]),
2905            elements: vec![ok_binding],
2906        },
2907        guard: None,
2908        body: rust::Expr::Block(rust::Block {
2909            statements: ok_arm_statements,
2910        }),
2911    }];
2912    for raise in &op.raises {
2913        let raise_variant = rust::escape_keyword(&raise.name);
2914        result_arms.push(rust::MatchArm {
2915            pattern: rust::Pattern::TupleStruct {
2916                path: Path::new(&[&result_name, &raise_variant]),
2917                elements: vec![rust::Pattern::Binding(Ident::new("exc"))],
2918            },
2919            guard: None,
2920            body: rust::Expr::Call {
2921                callee: Box::new(rust::Expr::Path(Path::single("Err"))),
2922                args: vec![rust::Expr::Call {
2923                    callee: Box::new(rust::Expr::Path(Path::new(&[
2924                        &error_type_name(service),
2925                        &raise_variant,
2926                    ]))),
2927                    args: vec![rust::Expr::Path(Path::single("exc"))],
2928                }],
2929            },
2930        });
2931    }
2932
2933    // §7.7.2.5.6: an Enhanced `Result` that carries an exception member
2934    // this version of the interface does not declare is
2935    // `REMOTE_EX_UNKNOWN_EXCEPTION` for the client.
2936    if service.mapping.is_enhanced() {
2937        result_arms.push(rust::MatchArm {
2938            pattern: rust::Pattern::TupleStruct {
2939                path: Path::new(&[&result_name, UNKNOWN_EX_VARIANT]),
2940                elements: vec![rust::Pattern::Wildcard],
2941            },
2942            guard: None,
2943            body: rust::Expr::Call {
2944                callee: Box::new(rust::Expr::Path(Path::single("Err"))),
2945                args: vec![rust::Expr::Call {
2946                    callee: Box::new(rust::Expr::Path(Path::new(&[
2947                        &error_type_name(service),
2948                        "Remote",
2949                    ]))),
2950                    args: vec![rust::Expr::Call {
2951                        callee: Box::new(rust::Expr::Path(Path::new(&[
2952                            "zerodds_rpc",
2953                            "error",
2954                            "RpcError",
2955                            "RemoteException",
2956                        ]))),
2957                        args: vec![rust::Expr::MethodCall {
2958                            receiver: Box::new(rust::Expr::Path(remote_exception_code(
2959                                "UnknownException",
2960                            ))),
2961                            method: Ident::new("as_u32"),
2962                            args: Vec::new(),
2963                        }],
2964                    }],
2965                }],
2966            },
2967        });
2968    }
2969
2970    let return_name = interface::return_union_name(&service.name);
2971    let outer_match = rust::Expr::Match {
2972        scrutinee: Box::new(rust::Expr::Path(Path::single("reply"))),
2973        arms: vec![
2974            rust::MatchArm {
2975                pattern: rust::Pattern::TupleStruct {
2976                    path: Path::new(&[&return_name, variant]),
2977                    elements: vec![rust::Pattern::Binding(Ident::new("result"))],
2978                },
2979                guard: None,
2980                body: rust::Expr::Match {
2981                    scrutinee: Box::new(rust::Expr::Path(Path::single("result"))),
2982                    arms: result_arms,
2983                },
2984            },
2985            rust::MatchArm {
2986                pattern: rust::Pattern::Wildcard,
2987                guard: None,
2988                body: rust::Expr::Call {
2989                    callee: Box::new(rust::Expr::Path(Path::single("Err"))),
2990                    args: vec![rust::Expr::Call {
2991                        callee: Box::new(rust::Expr::Path(Path::new(&[
2992                            &error_type_name(service),
2993                            "Remote",
2994                        ]))),
2995                        args: vec![rust::Expr::Call {
2996                            callee: Box::new(rust::Expr::Path(Path::new(&[
2997                                "zerodds_rpc",
2998                                "error",
2999                                "RpcError",
3000                                "Dcps",
3001                            ]))),
3002                            args: vec![rust::Expr::MethodCall {
3003                                receiver: Box::new(rust::Expr::Literal(Literal::string(&format!(
3004                                    "reply carried a case other than `{}`",
3005                                    op.name
3006                                )))),
3007                                method: Ident::new("to_string"),
3008                                args: Vec::new(),
3009                            }],
3010                        }],
3011                    }],
3012                },
3013            },
3014        ],
3015    };
3016    Ok(rust::Statement::Tail(outer_match))
3017}
3018
3019// -- the dispatcher -----------------------------------------------------------
3020
3021fn dispatcher_type_name(service: &Service) -> String {
3022    format!("{}Dispatcher", service.name)
3023}
3024
3025fn remote_exception_code(variant: &str) -> Path {
3026    Path::new(&[
3027        "zerodds_rpc",
3028        "common_types",
3029        "RemoteExceptionCode",
3030        variant,
3031    ])
3032}
3033
3034fn service_bound(service: &Service) -> Vec<rust::Type> {
3035    vec![
3036        rust::Type::named(Path::single(&rust::escape_keyword(&service.name))),
3037        rust::Type::named(Path::single("Send")),
3038        rust::Type::named(Path::single("Sync")),
3039        rust::Type::named(Path::single("'static")),
3040    ]
3041}
3042
3043fn dispatcher_generic() -> rust::GenericParam {
3044    // Filled in per service by the caller — see `dispatcher_struct`.
3045    rust::GenericParam {
3046        name: Ident::new("S"),
3047        bounds: Vec::new(),
3048        lifetime: false,
3049    }
3050}
3051
3052fn dispatcher_struct(service: &Service) -> rust::Struct {
3053    let mut generic = dispatcher_generic();
3054    generic.bounds = service_bound(service);
3055    let mut doc =
3056        Doc::line("Serves one service implementation over a `zerodds_rpc::replier::Replier`.");
3057    doc.push(
3058        "A base interface's operations are dispatched by that base's own \
3059         `Dispatcher` and `Replier`, run alongside this one — DDS-RPC 1.0 keeps \
3060         a base's topic separate from a derived interface's own (§7.5.1.1.8), \
3061         and so does this binding.",
3062    );
3063    rust::Struct {
3064        body_text: zerodds_lang_cst::common::BodyText::none(),
3065        doc,
3066        attrs: Vec::new(),
3067        derives: Vec::new(),
3068        visibility: Visibility::Public,
3069        name: Ident::new(&dispatcher_type_name(service)),
3070        generics: vec![generic],
3071        body: rust::StructBody::Named(vec![field_vis(
3072            "service",
3073            rust::Type::named(Path::single("S")),
3074            Visibility::Private,
3075            "The implementation this dispatcher hands each `${I}_Call` to — \
3076             DDS-RPC 1.0 §7.10's Replier, over the `${I}` trait.",
3077        )]),
3078    }
3079}
3080
3081fn dispatcher_out_literal(
3082    service: &Service,
3083    op: &Operation,
3084    qualify: &impl Fn(&str) -> Result<Path, RustInterfaceError>,
3085) -> Result<rust::Expr, RustInterfaceError> {
3086    let map = out_field_map(op);
3087    let out_path = qualify(&interface::out_struct_name(&service.name, &op.name))?;
3088    if map.is_empty() {
3089        let fields = if service.mapping.empty_style() == interface::EmptyOperationStyle::DummyMember
3090        {
3091            vec![(
3092                Ident::new(interface::DUMMY_MEMBER),
3093                rust::Expr::Literal(Literal::int(0)),
3094            )]
3095        } else {
3096            Vec::new()
3097        };
3098        return Ok(rust::Expr::StructLit {
3099            path: out_path,
3100            fields,
3101            base: None,
3102        });
3103    }
3104    let positions = ok_positions(op);
3105    let mut fields = Vec::new();
3106    for (field_name, source) in &map {
3107        let expr = match source {
3108            OutFieldSource::InOut(param_name) => {
3109                rust::Expr::Path(Path::single(&rust::escape_keyword(param_name)))
3110            }
3111            OutFieldSource::Return | OutFieldSource::Out(_) => {
3112                ok_component_expr(&positions, source, "value", op)?
3113            }
3114        };
3115        fields.push((Ident::new(field_name), expr));
3116    }
3117    Ok(rust::Expr::StructLit {
3118        path: out_path,
3119        fields,
3120        base: None,
3121    })
3122}
3123
3124/// The dispatcher call arm's binding for the `${I}_${op}_In` payload: a
3125/// name where `dispatcher_call_args`/`dispatcher_inout_locals` read one of
3126/// its fields (any `in`/`inout` parameter), else a wildcard — an operation
3127/// with none, such as a no-argument `reset()`, binds nothing so `rustc`
3128/// does not warn on the unread name.
3129fn in_binding_pattern(op: &Operation) -> rust::Pattern {
3130    let reads_in = op
3131        .params
3132        .iter()
3133        .any(|p| matches!(p.direction, ParamDirection::In | ParamDirection::InOut));
3134    if reads_in {
3135        rust::Pattern::Binding(Ident::new("in_"))
3136    } else {
3137        rust::Pattern::Wildcard
3138    }
3139}
3140
3141fn dispatcher_call_args(op: &Operation) -> Vec<rust::Expr> {
3142    let mut args = Vec::new();
3143    for p in &op.params {
3144        let name = rust::escape_keyword(&p.name);
3145        match p.direction {
3146            ParamDirection::In => args.push(field_expr("in_", &name)),
3147            ParamDirection::InOut => args.push(rust::Expr::Ref {
3148                target: Box::new(rust::Expr::Path(Path::single(&name))),
3149                mutable: true,
3150            }),
3151            ParamDirection::Out => {}
3152        }
3153    }
3154    args
3155}
3156
3157fn dispatcher_inout_locals(op: &Operation) -> Vec<rust::Statement> {
3158    op.params
3159        .iter()
3160        .filter(|p| matches!(p.direction, ParamDirection::InOut))
3161        .map(|p| {
3162            let name = rust::escape_keyword(&p.name);
3163            rust::Statement::Let {
3164                name: Ident::new(&name),
3165                type_: None,
3166                value: Some(field_expr("in_", &name)),
3167                mutable: true,
3168            }
3169        })
3170        .collect()
3171}
3172
3173/// One `${I}_Call` variant's arm in the dispatcher's `handle`.
3174fn dispatcher_call_arm(
3175    service: &Service,
3176    op: &Operation,
3177) -> Result<rust::MatchArm, RustInterfaceError> {
3178    dispatcher_call_arm_via(service, service, op, &|name| Ok(Path::single(name)))
3179}
3180
3181/// The arm of `dispatching`'s dispatcher for `op`, which `declaring`
3182/// declares: the same interface for an operation of its own, a base for an
3183/// inherited one (DDS-RPC 1.0 §7.5.1.2.6: the Enhanced mapping's request
3184/// choice holds every inherited operation, and the derived service
3185/// implements the base trait). The topic types are `dispatching`'s; the
3186/// `Result`, `Out` and error types are `declaring`'s, reached through
3187/// `qualify`.
3188fn dispatcher_call_arm_via(
3189    dispatching: &Service,
3190    declaring: &Service,
3191    op: &Operation,
3192    qualify: &impl Fn(&str) -> Result<Path, RustInterfaceError>,
3193) -> Result<rust::MatchArm, RustInterfaceError> {
3194    let service = dispatching;
3195    let variant = variant_name(&op.name);
3196    let call_name = interface::call_union_name(&service.name);
3197    let return_name = interface::return_union_name(&service.name);
3198
3199    let mut statements = dispatcher_inout_locals(op);
3200    let call_expr = rust::Expr::MethodCall {
3201        receiver: Box::new(field_expr("self", "service")),
3202        method: Ident::new(&rust::escape_keyword(&op.name)),
3203        args: dispatcher_call_args(op),
3204    };
3205
3206    let ok_pattern = if ok_positions(op).is_empty() {
3207        rust::Pattern::Wildcard
3208    } else {
3209        rust::Pattern::Binding(Ident::new("value"))
3210    };
3211    let result_name = interface::result_union_name(&declaring.name, &op.name);
3212    let ok_arm = rust::MatchArm {
3213        pattern: rust::Pattern::TupleStruct {
3214            path: Path::single("Ok"),
3215            elements: vec![ok_pattern],
3216        },
3217        guard: None,
3218        body: rust::Expr::Call {
3219            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
3220            args: vec![rust::Expr::Call {
3221                callee: Box::new(rust::Expr::Path(Path::new(&[&return_name, &variant]))),
3222                args: vec![rust::Expr::Call {
3223                    callee: Box::new(rust::Expr::Path(qualify(&result_name)?.join("Ok"))),
3224                    args: vec![dispatcher_out_literal(declaring, op, qualify)?],
3225                }],
3226            }],
3227        },
3228    };
3229    let mut arms = vec![ok_arm];
3230    for raise in &op.raises {
3231        let raise_variant = rust::escape_keyword(&raise.name);
3232        arms.push(rust::MatchArm {
3233            pattern: rust::Pattern::TupleStruct {
3234                path: Path::single("Err"),
3235                elements: vec![rust::Pattern::TupleStruct {
3236                    path: qualify(&error_type_name(declaring))?.join(&raise_variant),
3237                    elements: vec![rust::Pattern::Binding(Ident::new("exc"))],
3238                }],
3239            },
3240            guard: None,
3241            body: rust::Expr::Call {
3242                callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
3243                args: vec![rust::Expr::Call {
3244                    callee: Box::new(rust::Expr::Path(Path::new(&[&return_name, &variant]))),
3245                    args: vec![rust::Expr::Call {
3246                        callee: Box::new(rust::Expr::Path(
3247                            qualify(&result_name)?.join(&raise_variant),
3248                        )),
3249                        args: vec![rust::Expr::Path(Path::single("exc"))],
3250                    }],
3251                }],
3252            },
3253        });
3254    }
3255    // Exhaustive over `{svc}Error` whatever else it carries: a
3256    // `Remote(_)` this service should not have produced, or an
3257    // exception variant `op` does not declare because some *other*
3258    // operation of the same interface raises it — `{svc}Error` is
3259    // shared. Either is an exception the caller's declared set does not
3260    // name; §7.5.2 makes that `REMOTE_EX_UNKNOWN_EXCEPTION`.
3261    arms.push(rust::MatchArm {
3262        pattern: rust::Pattern::Wildcard,
3263        guard: None,
3264        body: rust::Expr::Call {
3265            callee: Box::new(rust::Expr::Path(Path::single("Err"))),
3266            args: vec![rust::Expr::Path(remote_exception_code("UnknownException"))],
3267        },
3268    });
3269    statements.push(rust::Statement::Tail(rust::Expr::Match {
3270        scrutinee: Box::new(call_expr),
3271        arms,
3272    }));
3273
3274    Ok(rust::MatchArm {
3275        pattern: rust::Pattern::TupleStruct {
3276            path: Path::new(&[&call_name, &variant]),
3277            elements: vec![in_binding_pattern(op)],
3278        },
3279        guard: None,
3280        body: rust::Expr::Block(rust::Block { statements }),
3281    })
3282}
3283
3284fn dispatcher_handle_fn(
3285    service: &Service,
3286    ops: &[Operation],
3287) -> Result<rust::Item, RustInterfaceError> {
3288    let call_name = interface::call_union_name(&service.name);
3289    let mut arms: Vec<rust::MatchArm> = ops
3290        .iter()
3291        .map(|op| dispatcher_call_arm(service, op))
3292        .collect::<Result<_, _>>()?;
3293    arms.push(rust::MatchArm {
3294        pattern: rust::Pattern::TupleStruct {
3295            path: Path::new(&[&call_name, UNKNOWN_OP_VARIANT]),
3296            elements: vec![rust::Pattern::Wildcard],
3297        },
3298        guard: None,
3299        body: rust::Expr::Call {
3300            callee: Box::new(rust::Expr::Path(Path::single("Err"))),
3301            args: vec![rust::Expr::Path(remote_exception_code("Unsupported"))],
3302        },
3303    });
3304    Ok(rust::Item::Function(rust::Function {
3305        doc: Doc::none(),
3306        attrs: Vec::new(),
3307        visibility: Visibility::Private,
3308        name: Ident::new("handle"),
3309        generics: Vec::new(),
3310        params: vec![
3311            rust::Param {
3312                name: None,
3313                type_: rust::Type::unit(),
3314                receiver: Some(rust::Receiver::Ref),
3315            },
3316            rust::Param {
3317                name: Some(Ident::new("request")),
3318                type_: rust::Type::named(Path::single(&call_name)),
3319                receiver: None,
3320            },
3321        ],
3322        returns: Some(rust::Type::generic(
3323            Path::single("Result"),
3324            vec![
3325                rust::Type::named(Path::single(&interface::return_union_name(&service.name))),
3326                rust::Type::named(Path::new(&[
3327                    "zerodds_rpc",
3328                    "common_types",
3329                    "RemoteExceptionCode",
3330                ])),
3331            ],
3332        )),
3333        body: Some(rust::Block {
3334            statements: vec![rust::Statement::Tail(rust::Expr::Match {
3335                scrutinee: Box::new(rust::Expr::Path(Path::single("request"))),
3336                arms,
3337            })],
3338        }),
3339    }))
3340}
3341
3342fn dispatcher_impl(ast: &Ast, service: &Service) -> Result<Vec<rust::Item>, RustInterfaceError> {
3343    let _ = ast;
3344    let mut generic = dispatcher_generic();
3345    generic.bounds = service_bound(service);
3346    let self_type = rust::Type::generic(
3347        Path::single(&dispatcher_type_name(service)),
3348        vec![rust::Type::named(Path::single("S"))],
3349    );
3350
3351    let new_fn = rust::Item::Function(rust::Function {
3352        doc: Doc::line("Wraps a service implementation, ready to serve."),
3353        attrs: Vec::new(),
3354        visibility: Visibility::Public,
3355        name: Ident::new("new"),
3356        generics: Vec::new(),
3357        params: vec![rust::Param {
3358            name: Some(Ident::new("service")),
3359            type_: rust::Type::named(Path::single("S")),
3360            receiver: None,
3361        }],
3362        returns: Some(rust::Type::named(Path::single("Self"))),
3363        body: Some(rust::Block {
3364            statements: vec![rust::Statement::Tail(rust::Expr::StructLit {
3365                path: Path::single("Self"),
3366                fields: vec![(
3367                    Ident::new("service"),
3368                    rust::Expr::Path(Path::single("service")),
3369                )],
3370                base: None,
3371            })],
3372        }),
3373    });
3374
3375    let call_name = interface::call_union_name(&service.name);
3376    let return_name = interface::return_union_name(&service.name);
3377    let serve_fn = rust::Item::Function(rust::Function {
3378        doc: Doc::line(
3379            "Registers a `Replier` for this interface level and starts serving. \
3380             Ticking it — reading requests, calling the service, writing replies \
3381             — is the caller's own event loop, as `zerodds_rpc::replier::Replier` \
3382             always leaves it.",
3383        ),
3384        attrs: Vec::new(),
3385        visibility: Visibility::Public,
3386        name: Ident::new("serve"),
3387        generics: Vec::new(),
3388        params: vec![
3389            rust::Param {
3390                name: None,
3391                type_: rust::Type::unit(),
3392                receiver: Some(rust::Receiver::Value),
3393            },
3394            rust::Param {
3395                name: Some(Ident::new("participant")),
3396                type_: dcps_participant_type(),
3397                receiver: None,
3398            },
3399            rust::Param {
3400                name: Some(Ident::new("qos")),
3401                type_: rpc_qos_type(),
3402                receiver: None,
3403            },
3404        ],
3405        returns: Some(rpc_result_of(rust::Type::generic(
3406            Path::new(&["zerodds_rpc", "replier", "Replier"]),
3407            vec![
3408                rust::Type::named(Path::single(&call_name)),
3409                rust::Type::named(Path::single(&return_name)),
3410            ],
3411        ))),
3412        body: Some(rust::Block {
3413            statements: vec![rust::Statement::Tail(rust::Expr::Call {
3414                callee: Box::new(rust::Expr::Path(Path::new(&[
3415                    "zerodds_rpc",
3416                    "replier",
3417                    "Replier",
3418                    "with_options",
3419                ]))),
3420                args: vec![
3421                    rust::Expr::Path(Path::single("participant")),
3422                    rust::Expr::Literal(Literal::string(&service.qualified_name)),
3423                    rust::Expr::Literal(Literal::string("")),
3424                    rust::Expr::Path(Path::single("qos")),
3425                    binding_style_expr(),
3426                    annotated_topics_expr(service),
3427                    endpoint_options_expr(ast, service)?,
3428                    rust::Expr::Call {
3429                        callee: Box::new(rust::Expr::Path(Path::new(&[
3430                            "std", "sync", "Arc", "new",
3431                        ]))),
3432                        args: vec![rust::Expr::Path(Path::single("self"))],
3433                    },
3434                ],
3435            })],
3436        }),
3437    });
3438
3439    let inherent = rust::Item::Impl(rust::Impl {
3440        doc: Doc::none(),
3441        attrs: Vec::new(),
3442        generics: vec![generic.clone()],
3443        trait_: None,
3444        self_type: self_type.clone(),
3445        items: {
3446            let mut items = vec![new_fn, serve_fn];
3447            if let Some(request_profile) = service.qos_request_profile.as_deref() {
3448                items.push(declared_qos_fn(
3449                    "serve_with_declared_qos",
3450                    "Like `serve`, with the QoS `@Qos` declares for this interface (DDS-RPC 1.0 §7.10.1).",
3451                    request_profile,
3452                    service.qos_reply_profile.as_deref(),
3453                    Some(rust::Receiver::Value),
3454                    rpc_result_of(rust::Type::generic(
3455                        Path::new(&["zerodds_rpc", "replier", "Replier"]),
3456                        vec![
3457                            rust::Type::named(Path::single(&call_name)),
3458                            rust::Type::named(Path::single(&return_name)),
3459                        ],
3460                    )),
3461                    &["Self", "serve"],
3462                ));
3463            }
3464            items
3465        },
3466    });
3467
3468    let ops = service.all_operations();
3469    let handler = rust::Item::Impl(rust::Impl {
3470        doc: Doc::none(),
3471        attrs: Vec::new(),
3472        generics: vec![generic],
3473        trait_: Some(rust::Type::generic(
3474            Path::new(&["zerodds_rpc", "replier", "ReplierHandler"]),
3475            vec![
3476                rust::Type::named(Path::single(&call_name)),
3477                rust::Type::named(Path::single(&return_name)),
3478            ],
3479        )),
3480        self_type,
3481        items: vec![dispatcher_handle_fn(service, &ops)?],
3482    });
3483
3484    Ok(vec![inherent, handler])
3485}