Skip to main content

zerodds_idl_java/
rpc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! IDL service → Java RPC codegen (DDS-RPC 1.0 §7.11.2 — Java PSM).
4//!
5//! This module is the bridge between the typed RPC data model
6//! from `zerodds-rpc` (`ServiceDef`/`MethodDef`/`ParamDef`) and the
7//! Java source codegen. Per service we emit four classes:
8//!
9//! * `<Service>.java`         — synchronous interface with all methods
10//!                              (`throws RemoteException` + user exceptions).
11//! * `<Service>Async.java`    — asynchronous interface with
12//!                              `CompletableFuture<TOut>` returns
13//!                              (spec §7.11.2.2.4 maps `Future<T>` to
14//!                              `java.util.concurrent.Future<T>`; we
15//!                              use `CompletableFuture` as a
16//!                              full implementation).
17//! * `<Service>Requester.java` — client side: wraps a
18//!                              `org.zerodds.rpc.Requester<TIn,TOut>`
19//!                              and implements `<Service>` +
20//!                              `<Service>Async`.
21//! * `<Service>Replier.java`   — server side: wraps a
22//!                              `org.zerodds.rpc.Replier<TIn,TOut>` and
23//!                              dispatches incoming requests to a
24//!                              `<Service>Service` handler.
25//! * `<Service>Service.java`   — server-side handler interface: the
26//!                              service implementor implements this
27//!                              interface.
28//!
29//! # Out parameters
30//! Java has no `out`/`inout` concept. We map `out`/`inout` via the
31//! **holder pattern** (spec §7.11.2.3 / IDL-to-Java 1.3 §1.5):
32//!
33//! ```java
34//! public final class IntHolder {
35//!     public int value;
36//!     public IntHolder() {}
37//!     public IntHolder(int v) { this.value = v; }
38//! }
39//! ```
40//!
41//! Holders are **not** emitted per service — we reference the
42//! generic holders from `org.zerodds.rpc.Holder<T>` (see
43//! `runtime/rpc/Holder.java`). Rationale: less boilerplate than
44//! per-type holders, fully compatible with the spec pattern (the
45//! caller writes to `holder.value` before the call, the reply decoder
46//! overwrites it on return). The alternative `Object[]` wrapper would be
47//! type-unsafe and is therefore ruled out.
48//!
49//! # Exception mapping (spec §7.11.2.1)
50//! * IDL `exception E { ... }` → we delegate to the existing
51//!   `emit_exception_file` path (`E extends RuntimeException`); the RPC
52//!   codegen only adds the `throws E1, E2` clause on the method.
53//! * `org.zerodds.rpc.RemoteException` is a RuntimeException
54//!   subclass that every RPC method may throw implicitly — we therefore
55//!   do **not** place it explicitly in the `throws` lists, because
56//!   RuntimeException is not checked in Java.
57//!
58//! # Marshalling convention
59//! The requester/replier marshal a type-erased tuple
60//! through the runtime `Requester<Object,Object>` / `Replier<Object,Object>`:
61//! the request payload is an `Object[]` of the IN + INOUT values (declaration
62//! order); the reply payload is an `Object[] { returnValue-or-null, INOUT+OUT
63//! values… }`. The requester writes the INOUT/OUT holders back from the reply
64//! and casts the return value; the replier decodes the request tuple, builds
65//! holders, calls the handler, and packs the reply. The generated code is
66//! verified to compile against the real runtime by
67//! `tests/compile_check.rs` (real `javac`, no stubs).
68//!
69//! # What this stage does NOT do
70//! * No `mvn` packaging — callers drop the `runtime/` sources (or a jar)
71//!   alongside the generated code (see `runtime/README.md`).
72//! * No reflection TypeRep (a stretch in idl4-java §8).
73
74extern crate alloc;
75
76use alloc::format;
77use alloc::string::{String, ToString};
78use alloc::vec::Vec;
79use core::fmt::Write;
80
81use zerodds_idl::ast::{IntegerType, PrimitiveType, ScopedName, TypeSpec};
82use zerodds_rpc::service_mapping::{MethodDef, ParamDef, ParamDirection, ServiceDef};
83
84use crate::JavaGenOptions;
85use crate::emitter::{JavaFile, fmt_err, indent_unit, wrap_compilation_unit_default};
86use crate::error::JavaGenError;
87use crate::keywords::sanitize_identifier;
88use crate::type_map::{
89    floating_to_java, floating_to_java_boxed, integer_to_java, integer_to_java_boxed,
90    primitive_to_java, primitive_to_java_boxed,
91};
92
93// ---------------------------------------------------------------------------
94// Public API — see the crate doc for the call schema
95// ---------------------------------------------------------------------------
96
97/// Emits the synchronous service interface (`<Service>.java`).
98///
99/// # Errors
100/// `JavaGenError::InvalidName` for a non-sanitizable service name.
101pub fn emit_service_interface(
102    svc: &ServiceDef,
103    pkg: &str,
104    opts: &JavaGenOptions,
105) -> Result<JavaFile, JavaGenError> {
106    let class = sanitize_identifier(&svc.name)?;
107    let ind = indent_unit(opts);
108    let mut body = String::new();
109    writeln!(body, "/** Synchronous service interface for {class}. */").map_err(fmt_err)?;
110    writeln!(body, "@org.zerodds.rpc.Service(\"{}\")", svc.name).map_err(fmt_err)?;
111    writeln!(body, "public interface {class} {{").map_err(fmt_err)?;
112    for m in &svc.methods {
113        emit_sync_method_signature(&mut body, m, &ind)?;
114    }
115    writeln!(body, "}}").map_err(fmt_err)?;
116    let source = wrap_compilation_unit_default(pkg, &body);
117    Ok(JavaFile {
118        package_path: pkg.to_string(),
119        class_name: class,
120        source,
121    })
122}
123
124/// Emits the asynchronous service interface (`<Service>Async.java`).
125///
126/// # Errors
127/// Like [`emit_service_interface`].
128pub fn emit_service_interface_async(
129    svc: &ServiceDef,
130    pkg: &str,
131    opts: &JavaGenOptions,
132) -> Result<JavaFile, JavaGenError> {
133    let svc_class = sanitize_identifier(&svc.name)?;
134    let class = format!("{svc_class}Async");
135    let ind = indent_unit(opts);
136    let mut body = String::new();
137    writeln!(
138        body,
139        "/** Asynchronous service interface for {svc_class}. */"
140    )
141    .map_err(fmt_err)?;
142    writeln!(body, "public interface {class} {{").map_err(fmt_err)?;
143    for m in &svc.methods {
144        emit_async_method_signature(&mut body, m, &ind)?;
145    }
146    writeln!(body, "}}").map_err(fmt_err)?;
147    let source = wrap_compilation_unit_default(pkg, &body);
148    Ok(JavaFile {
149        package_path: pkg.to_string(),
150        class_name: class,
151        source,
152    })
153}
154
155/// Emits the client-side requester class (`<Service>Requester.java`).
156///
157/// # Errors
158/// Like [`emit_service_interface`].
159pub fn emit_requester_class(
160    svc: &ServiceDef,
161    pkg: &str,
162    opts: &JavaGenOptions,
163) -> Result<JavaFile, JavaGenError> {
164    let svc_class = sanitize_identifier(&svc.name)?;
165    let class = format!("{svc_class}Requester");
166    let ind = indent_unit(opts);
167    let mut body = String::new();
168    writeln!(
169        body,
170        "/** Client-side proxy for {svc_class}. Implements both the \
171         synchronous {svc_class} interface and the {svc_class}Async \
172         interface. */",
173    )
174    .map_err(fmt_err)?;
175    writeln!(
176        body,
177        "public final class {class} implements {svc_class}, {svc_class}Async {{",
178    )
179    .map_err(fmt_err)?;
180    writeln!(
181        body,
182        "{ind}private final org.zerodds.rpc.Requester<Object, Object> requester;",
183    )
184    .map_err(fmt_err)?;
185    writeln!(body).map_err(fmt_err)?;
186    writeln!(
187        body,
188        "{ind}public {class}(org.zerodds.rpc.Requester<Object, Object> requester) {{",
189    )
190    .map_err(fmt_err)?;
191    writeln!(body, "{ind}{ind}this.requester = requester;").map_err(fmt_err)?;
192    writeln!(body, "{ind}}}").map_err(fmt_err)?;
193    writeln!(body).map_err(fmt_err)?;
194
195    // Sync methods: call the async variant and block.
196    for m in &svc.methods {
197        emit_requester_sync_impl(&mut body, m, &ind)?;
198    }
199    writeln!(body).map_err(fmt_err)?;
200    // Async methods: send the request, return a CompletableFuture.
201    for m in &svc.methods {
202        emit_requester_async_impl(&mut body, m, &ind)?;
203    }
204
205    writeln!(body, "}}").map_err(fmt_err)?;
206    let source = wrap_compilation_unit_default(pkg, &body);
207    Ok(JavaFile {
208        package_path: pkg.to_string(),
209        class_name: class,
210        source,
211    })
212}
213
214/// Emits the server-side replier class (`<Service>Replier.java`).
215///
216/// # Errors
217/// Like [`emit_service_interface`].
218pub fn emit_replier_class(
219    svc: &ServiceDef,
220    pkg: &str,
221    opts: &JavaGenOptions,
222) -> Result<JavaFile, JavaGenError> {
223    let svc_class = sanitize_identifier(&svc.name)?;
224    let class = format!("{svc_class}Replier");
225    let handler_iface = format!("{svc_class}Service");
226    let ind = indent_unit(opts);
227    let mut body = String::new();
228    writeln!(
229        body,
230        "/** Server-side replier for {svc_class}. Wires a {handler_iface} \
231         implementation to the underlying RPC runtime. */",
232    )
233    .map_err(fmt_err)?;
234    writeln!(body, "public final class {class} {{").map_err(fmt_err)?;
235    writeln!(
236        body,
237        "{ind}private final org.zerodds.rpc.Replier<Object, Object> replier;",
238    )
239    .map_err(fmt_err)?;
240    writeln!(body, "{ind}private final {handler_iface} handler;").map_err(fmt_err)?;
241    writeln!(body).map_err(fmt_err)?;
242    writeln!(
243        body,
244        "{ind}public {class}(org.zerodds.rpc.Replier<Object, Object> replier, {handler_iface} handler) {{",
245    )
246    .map_err(fmt_err)?;
247    writeln!(body, "{ind}{ind}this.replier = replier;").map_err(fmt_err)?;
248    writeln!(body, "{ind}{ind}this.handler = handler;").map_err(fmt_err)?;
249    writeln!(body, "{ind}}}").map_err(fmt_err)?;
250    writeln!(body).map_err(fmt_err)?;
251
252    // Dispatch stub: takes a request and calls the right
253    // the handler method by method ID.
254    writeln!(
255        body,
256        "{ind}/** Dispatches an incoming request by method id. */",
257    )
258    .map_err(fmt_err)?;
259    writeln!(
260        body,
261        "{ind}public Object dispatch(int methodId, Object args) {{",
262    )
263    .map_err(fmt_err)?;
264    writeln!(body, "{ind}{ind}switch (methodId) {{").map_err(fmt_err)?;
265    for (idx, m) in svc.methods.iter().enumerate() {
266        let mname = sanitize_identifier(&m.name)?;
267        let case_id = idx + 1;
268        // Decode the request tuple, call the handler, pack the reply tuple —
269        // mirroring the requester marshalling convention:
270        //   args  = Object[] of IN + INOUT values (declaration order)
271        //   reply = Object[] { returnValue-or-null, INOUT+OUT values… }
272        writeln!(body, "{ind}{ind}{ind}case {case_id}: {{").map_err(fmt_err)?;
273        writeln!(body, "{ind}{ind}{ind}{ind}Object[] __a = (Object[]) args;").map_err(fmt_err)?;
274        let mut req_idx = 0usize;
275        let mut call_args: Vec<String> = Vec::new();
276        for p in &m.params {
277            let pname = sanitize_identifier(&p.name)?;
278            let boxed = typespec_to_java_boxed(&p.type_ref)?;
279            match p.direction {
280                ParamDirection::In => {
281                    call_args.push(format!("({boxed}) __a[{req_idx}]"));
282                    req_idx += 1;
283                }
284                ParamDirection::InOut => {
285                    writeln!(
286                        body,
287                        "{ind}{ind}{ind}{ind}org.zerodds.rpc.Holder<{boxed}> {pname} = \
288                         new org.zerodds.rpc.Holder<>(({boxed}) __a[{req_idx}]);",
289                    )
290                    .map_err(fmt_err)?;
291                    call_args.push(pname);
292                    req_idx += 1;
293                }
294                ParamDirection::Out => {
295                    writeln!(
296                        body,
297                        "{ind}{ind}{ind}{ind}org.zerodds.rpc.Holder<{boxed}> {pname} = \
298                         new org.zerodds.rpc.Holder<>();",
299                    )
300                    .map_err(fmt_err)?;
301                    call_args.push(pname);
302                }
303            }
304        }
305        let call = format!("handler.{mname}({})", call_args.join(", "));
306        if m.return_type.is_some() {
307            let ret_unboxed = sync_return_type(m)?;
308            writeln!(body, "{ind}{ind}{ind}{ind}{ret_unboxed} __ret = {call};").map_err(fmt_err)?;
309        } else {
310            writeln!(body, "{ind}{ind}{ind}{ind}{call};").map_err(fmt_err)?;
311        }
312        // Reply tuple: [0] = return value (or null), then inout/out values.
313        let mut reply_parts: Vec<String> = Vec::new();
314        reply_parts.push(if m.return_type.is_some() {
315            "__ret".to_string()
316        } else {
317            "null".to_string()
318        });
319        for p in &m.params {
320            if p.direction != ParamDirection::In {
321                reply_parts.push(format!("{}.value", sanitize_identifier(&p.name)?));
322            }
323        }
324        writeln!(
325            body,
326            "{ind}{ind}{ind}{ind}return new Object[] {{ {} }};",
327            reply_parts.join(", ")
328        )
329        .map_err(fmt_err)?;
330        writeln!(body, "{ind}{ind}{ind}}}").map_err(fmt_err)?;
331    }
332    writeln!(
333        body,
334        "{ind}{ind}{ind}default: throw new org.zerodds.rpc.RemoteException(\
335         \"unknown method id: \" + methodId, \
336         org.zerodds.rpc.RemoteExceptionCode.UNKNOWN_OPERATION);",
337    )
338    .map_err(fmt_err)?;
339    writeln!(body, "{ind}{ind}}}").map_err(fmt_err)?;
340    writeln!(body, "{ind}}}").map_err(fmt_err)?;
341
342    writeln!(body, "}}").map_err(fmt_err)?;
343    let source = wrap_compilation_unit_default(pkg, &body);
344    Ok(JavaFile {
345        package_path: pkg.to_string(),
346        class_name: class,
347        source,
348    })
349}
350
351/// Emits the server-side handler interface (`<Service>Service.java`).
352///
353/// Users implement this interface and pass their
354/// implementation to the [`emit_replier_class`] output.
355///
356/// # Errors
357/// Like [`emit_service_interface`].
358pub fn emit_service_handler_interface(
359    svc: &ServiceDef,
360    pkg: &str,
361    opts: &JavaGenOptions,
362) -> Result<JavaFile, JavaGenError> {
363    let svc_class = sanitize_identifier(&svc.name)?;
364    let class = format!("{svc_class}Service");
365    let ind = indent_unit(opts);
366    let mut body = String::new();
367    writeln!(
368        body,
369        "/** Server-side handler interface for {svc_class}. Implementors \
370         provide the actual business logic; a {svc_class}Replier wires \
371         them to the RPC runtime. */",
372    )
373    .map_err(fmt_err)?;
374    writeln!(body, "public interface {class} {{").map_err(fmt_err)?;
375    for m in &svc.methods {
376        emit_handler_method_signature(&mut body, m, &ind)?;
377    }
378    writeln!(body, "}}").map_err(fmt_err)?;
379    let source = wrap_compilation_unit_default(pkg, &body);
380    Ok(JavaFile {
381        package_path: pkg.to_string(),
382        class_name: class,
383        source,
384    })
385}
386
387/// Convenience wrapper: emits all 5 Java files for one service.
388///
389/// Order: interface, async interface, handler interface, requester,
390/// replier.
391///
392/// # Errors
393/// Like [`emit_service_interface`].
394pub fn emit_service_files(
395    svc: &ServiceDef,
396    pkg: &str,
397    opts: &JavaGenOptions,
398) -> Result<Vec<JavaFile>, JavaGenError> {
399    Ok(alloc::vec![
400        emit_service_interface(svc, pkg, opts)?,
401        emit_service_interface_async(svc, pkg, opts)?,
402        emit_service_handler_interface(svc, pkg, opts)?,
403        emit_requester_class(svc, pkg, opts)?,
404        emit_replier_class(svc, pkg, opts)?,
405    ])
406}
407
408// ---------------------------------------------------------------------------
409// Method-Signature-Emitter
410// ---------------------------------------------------------------------------
411
412fn emit_sync_method_signature(
413    out: &mut String,
414    m: &MethodDef,
415    ind: &str,
416) -> Result<(), JavaGenError> {
417    let name = sanitize_identifier(&m.name)?;
418    let ret = sync_return_type(m)?;
419    let params = render_method_params(m)?;
420    let throws = String::new();
421    if m.oneway {
422        writeln!(out, "{ind}@org.zerodds.rpc.Oneway").map_err(fmt_err)?;
423    }
424    writeln!(out, "{ind}{ret} {name}({params}){throws};").map_err(fmt_err)?;
425    Ok(())
426}
427
428fn emit_async_method_signature(
429    out: &mut String,
430    m: &MethodDef,
431    ind: &str,
432) -> Result<(), JavaGenError> {
433    let name = sanitize_identifier(&m.name)?;
434    let async_name = format!("{name}Async");
435    let ret = async_return_type(m)?;
436    let params = render_method_params_async(m)?;
437    if m.oneway {
438        writeln!(out, "{ind}@org.zerodds.rpc.Oneway").map_err(fmt_err)?;
439    }
440    writeln!(out, "{ind}{ret} {async_name}({params});").map_err(fmt_err)?;
441    Ok(())
442}
443
444fn emit_handler_method_signature(
445    out: &mut String,
446    m: &MethodDef,
447    ind: &str,
448) -> Result<(), JavaGenError> {
449    // The handler signature is identical to the sync signature — the
450    // Replier-Dispatch ruft sie direkt auf.
451    emit_sync_method_signature(out, m, ind)
452}
453
454fn emit_requester_sync_impl(
455    out: &mut String,
456    m: &MethodDef,
457    ind: &str,
458) -> Result<(), JavaGenError> {
459    let name = sanitize_identifier(&m.name)?;
460    let async_name = format!("{name}Async");
461    let ret_ty = sync_return_type(m)?;
462    let params = render_method_params(m)?;
463    let arg_list = render_call_arglist(m)?;
464    writeln!(out, "{ind}@Override").map_err(fmt_err)?;
465    writeln!(out, "{ind}public {ret_ty} {name}({params}) {{").map_err(fmt_err)?;
466    if m.oneway {
467        writeln!(out, "{ind}{ind}{async_name}({arg_list});").map_err(fmt_err)?;
468        writeln!(out, "{ind}}}").map_err(fmt_err)?;
469        return Ok(());
470    }
471    if m.return_type.is_none() {
472        // void return (with or without out/inout holders): block on the
473        // future but do not `return` its (Void) value.
474        writeln!(
475            out,
476            "{ind}{ind}try {{ {async_name}({arg_list}).get(); }} catch (Exception e) {{ \
477             throw new org.zerodds.rpc.RemoteException(e); }}",
478        )
479        .map_err(fmt_err)?;
480    } else {
481        writeln!(
482            out,
483            "{ind}{ind}try {{ return {async_name}({arg_list}).get(); }} catch (Exception e) {{ \
484             throw new org.zerodds.rpc.RemoteException(e); }}",
485        )
486        .map_err(fmt_err)?;
487    }
488    writeln!(out, "{ind}}}").map_err(fmt_err)?;
489    Ok(())
490}
491
492fn emit_requester_async_impl(
493    out: &mut String,
494    m: &MethodDef,
495    ind: &str,
496) -> Result<(), JavaGenError> {
497    let name = sanitize_identifier(&m.name)?;
498    let async_name = format!("{name}Async");
499    let ret_ty = async_return_type(m)?;
500    let params = render_method_params_async(m)?;
501    writeln!(out, "{ind}@Override").map_err(fmt_err)?;
502    writeln!(out, "{ind}public {ret_ty} {async_name}({params}) {{").map_err(fmt_err)?;
503
504    // Type-erased marshalling convention (matches the replier dispatch):
505    //   request payload  = Object[] of IN + INOUT values (declaration order)
506    //   reply payload    = Object[] { returnValue-or-null, INOUT+OUT values… }
507    let req_args = request_args_array(m)?;
508    if m.oneway {
509        writeln!(out, "{ind}{ind}requester.sendOneway({req_args});").map_err(fmt_err)?;
510        writeln!(
511            out,
512            "{ind}{ind}return java.util.concurrent.CompletableFuture.completedFuture(null);",
513        )
514        .map_err(fmt_err)?;
515    } else {
516        let has_writeback = m.params.iter().any(|p| p.direction != ParamDirection::In);
517        if m.return_type.is_none() && !has_writeback {
518            // void, no holders: complete with null once the reply arrives.
519            writeln!(
520                out,
521                "{ind}{ind}return requester.sendRequest({req_args}).thenApply(__reply -> null);",
522            )
523            .map_err(fmt_err)?;
524        } else {
525            writeln!(
526                out,
527                "{ind}{ind}return requester.sendRequest({req_args}).thenApply(__reply -> {{",
528            )
529            .map_err(fmt_err)?;
530            writeln!(out, "{ind}{ind}{ind}Object[] __out = (Object[]) __reply;",)
531                .map_err(fmt_err)?;
532            // Write back inout/out holders from reply[1..] in declaration order.
533            let mut k = 1usize;
534            for p in &m.params {
535                if p.direction == ParamDirection::In {
536                    continue;
537                }
538                let pname = sanitize_identifier(&p.name)?;
539                let boxed = typespec_to_java_boxed(&p.type_ref)?;
540                writeln!(out, "{ind}{ind}{ind}{pname}.value = ({boxed}) __out[{k}];",)
541                    .map_err(fmt_err)?;
542                k += 1;
543            }
544            match &m.return_type {
545                None => {
546                    writeln!(out, "{ind}{ind}{ind}return null;").map_err(fmt_err)?;
547                }
548                Some(ts) => {
549                    let boxed = typespec_to_java_boxed(ts)?;
550                    writeln!(out, "{ind}{ind}{ind}return ({boxed}) __out[0];").map_err(fmt_err)?;
551                }
552            }
553            writeln!(out, "{ind}{ind}}});").map_err(fmt_err)?;
554        }
555    }
556    writeln!(out, "{ind}}}").map_err(fmt_err)?;
557    Ok(())
558}
559
560// ---------------------------------------------------------------------------
561// Type-Mapping-Helpers
562// ---------------------------------------------------------------------------
563
564/// Returns the Java return type for the synchronous method signature.
565/// `oneway` and `void` → `void`. `out` parameters are mapped in the holder
566/// pattern — they remain part of the parameter list.
567fn sync_return_type(m: &MethodDef) -> Result<String, JavaGenError> {
568    if m.oneway {
569        return Ok("void".to_string());
570    }
571    match &m.return_type {
572        None => Ok("void".to_string()),
573        Some(ts) => typespec_to_java_unboxed(ts),
574    }
575}
576
577/// Returns the Java async return type. `oneway` yields
578/// `CompletableFuture<Void>` (the caller may still wait on "complete"
579/// even if the reply will be skipped), `void` → `Void`,
580/// otherwise `CompletableFuture<TBoxed>`.
581fn async_return_type(m: &MethodDef) -> Result<String, JavaGenError> {
582    if m.oneway {
583        return Ok("java.util.concurrent.CompletableFuture<Void>".to_string());
584    }
585    match &m.return_type {
586        None => Ok("java.util.concurrent.CompletableFuture<Void>".to_string()),
587        Some(ts) => Ok(format!(
588            "java.util.concurrent.CompletableFuture<{}>",
589            typespec_to_java_boxed(ts)?
590        )),
591    }
592}
593
594/// Parameter list for the sync variant. `out`/`inout` are
595/// Holder gerendert.
596fn render_method_params(m: &MethodDef) -> Result<String, JavaGenError> {
597    let mut parts: Vec<String> = Vec::new();
598    for p in &m.params {
599        parts.push(render_param(p)?);
600    }
601    Ok(parts.join(", "))
602}
603
604/// Parameter list for the async variant; the holder pattern is preserved.
605fn render_method_params_async(m: &MethodDef) -> Result<String, JavaGenError> {
606    render_method_params(m)
607}
608
609fn render_param(p: &ParamDef) -> Result<String, JavaGenError> {
610    let name = sanitize_identifier(&p.name)?;
611    let ty = match p.direction {
612        ParamDirection::In => typespec_to_java_unboxed(&p.type_ref)?,
613        ParamDirection::Out | ParamDirection::InOut => {
614            // Holder-Pattern (Spec §7.11.2 / IDL2Java 1.3 §1.5).
615            format!(
616                "org.zerodds.rpc.Holder<{}>",
617                typespec_to_java_boxed(&p.type_ref)?
618            )
619        }
620    };
621    Ok(format!("{ty} {name}"))
622}
623
624fn render_call_arglist(m: &MethodDef) -> Result<String, JavaGenError> {
625    let mut parts: Vec<String> = Vec::new();
626    for p in &m.params {
627        parts.push(sanitize_identifier(&p.name)?);
628    }
629    Ok(parts.join(", "))
630}
631
632/// `new Object[] { … }` request payload sent to the runtime `Requester`:
633/// IN params contribute their value, INOUT their `.value`, OUT nothing (the
634/// server allocates an empty holder). Order = declaration order.
635fn request_args_array(m: &MethodDef) -> Result<String, JavaGenError> {
636    let mut parts: Vec<String> = Vec::new();
637    for p in &m.params {
638        let pname = sanitize_identifier(&p.name)?;
639        match p.direction {
640            ParamDirection::In => parts.push(pname),
641            ParamDirection::InOut => parts.push(format!("{pname}.value")),
642            ParamDirection::Out => {}
643        }
644    }
645    Ok(format!("new Object[] {{ {} }}", parts.join(", ")))
646}
647
648// ---------------------------------------------------------------------------
649// TypeSpec → Java
650// ---------------------------------------------------------------------------
651
652fn typespec_to_java_unboxed(ts: &TypeSpec) -> Result<String, JavaGenError> {
653    match ts {
654        TypeSpec::Primitive(p) => Ok(primitive_to_java(*p).to_string()),
655        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
656        TypeSpec::String(_) => Ok("String".to_string()),
657        TypeSpec::Sequence(s) => Ok(format!(
658            "java.util.List<{}>",
659            typespec_to_java_boxed(&s.elem)?
660        )),
661        TypeSpec::Map(mm) => Ok(format!(
662            "java.util.Map<{}, {}>",
663            typespec_to_java_boxed(&mm.key)?,
664            typespec_to_java_boxed(&mm.value)?,
665        )),
666        TypeSpec::Fixed(_) => Err(JavaGenError::UnsupportedConstruct {
667            construct: "fixed".into(),
668            context: None,
669        }),
670        TypeSpec::Any => Err(JavaGenError::UnsupportedConstruct {
671            construct: "any".into(),
672            context: None,
673        }),
674    }
675}
676
677fn typespec_to_java_boxed(ts: &TypeSpec) -> Result<String, JavaGenError> {
678    match ts {
679        TypeSpec::Primitive(p) => Ok(primitive_to_java_boxed(*p).to_string()),
680        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
681        TypeSpec::String(_) => Ok("String".to_string()),
682        TypeSpec::Sequence(s) => Ok(format!(
683            "java.util.List<{}>",
684            typespec_to_java_boxed(&s.elem)?
685        )),
686        TypeSpec::Map(mm) => Ok(format!(
687            "java.util.Map<{}, {}>",
688            typespec_to_java_boxed(&mm.key)?,
689            typespec_to_java_boxed(&mm.value)?,
690        )),
691        TypeSpec::Fixed(_) => Err(JavaGenError::UnsupportedConstruct {
692            construct: "fixed".into(),
693            context: None,
694        }),
695        TypeSpec::Any => Err(JavaGenError::UnsupportedConstruct {
696            construct: "any".into(),
697            context: None,
698        }),
699    }
700}
701
702fn scoped_to_java(s: &ScopedName) -> String {
703    s.parts
704        .iter()
705        .map(|p| p.text.clone())
706        .collect::<Vec<_>>()
707        .join(".")
708}
709
710// Marker so the linter does not report the "maybe-later" helpers
711// as unused.
712#[allow(dead_code)]
713fn _unused_marker(_i: IntegerType) {
714    let _ = integer_to_java;
715    let _ = floating_to_java;
716    let _ = integer_to_java_boxed;
717    let _ = floating_to_java_boxed;
718    let _ = PrimitiveType::Boolean;
719}