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//! Dieser Modul ist die Bruecke zwischen dem typisierten RPC-Datenmodell
6//! aus `zerodds-rpc` (`ServiceDef`/`MethodDef`/`ParamDef`) und dem
7//! Java-Source-Codegen. Pro Service emittieren wir vier Klassen:
8//!
9//! * `<Service>.java`         — synchrones Interface mit allen Methoden
10//!                              (`throws RemoteException` + User-Exceptions).
11//! * `<Service>Async.java`    — asynchrones Interface mit
12//!                              `CompletableFuture<TOut>`-Returns
13//!                              (Spec §7.11.2.2.4 mappt `Future<T>` auf
14//!                              `java.util.concurrent.Future<T>`; wir
15//!                              nutzen `CompletableFuture` als
16//!                              vollstaendige Implementierung).
17//! * `<Service>Requester.java` — Client-Side: kapselt einen
18//!                              `org.zerodds.rpc.Requester<TIn,TOut>`
19//!                              und implementiert `<Service>` +
20//!                              `<Service>Async`.
21//! * `<Service>Replier.java`   — Server-Side: kapselt einen
22//!                              `org.zerodds.rpc.Replier<TIn,TOut>` und
23//!                              dispatcht eingehende Requests an einen
24//!                              `<Service>Service`-Handler.
25//! * `<Service>Service.java`   — Server-Side Handler-Interface: das
26//!                              Service-Implementor implementiert dieses
27//!                              Interface.
28//!
29//! # Out-Parameter
30//! Java hat kein `out`/`inout`-Konzept. Wir mappen `out`/`inout` ueber das
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//! Holder werden **nicht** pro Service emittiert — wir referenzieren die
42//! generischen Holders aus `org.zerodds.rpc.Holder<T>` (siehe
43//! `runtime/rpc/Holder.java`). Begruendung: weniger Boilerplate als
44//! pro-Type-Holder, vollstaendig kompatibel mit dem Spec-Pattern (der
45//! Caller schreibt vor dem Aufruf in `holder.value`, der Reply-Decoder
46//! ueberschreibt nach Rueckkehr). Alternative `Object[]`-Wrapper waere
47//! type-unsafe und scheidet daher aus.
48//!
49//! # Exception-Mapping (Spec §7.11.2.1)
50//! * IDL `exception E { ... }` → wir delegieren an den existierenden
51//!   `emit_exception_file`-Pfad (`E extends RuntimeException`); der RPC-
52//!   Codegen ergaenzt nur die `throws E1, E2`-Klausel auf der Methode.
53//! * `org.zerodds.rpc.RemoteException` ist eine RuntimeException-
54//!   Subclass, die jede RPC-Methode implizit werfen darf — wir setzen
55//!   sie deshalb **nicht** explizit in den `throws`-Listen, weil
56//!   RuntimeException in Java nicht checked ist.
57//!
58//! # Was diese Stufe NICHT macht
59//! * Keine `javac`/`mvn`-Integration — der Output ist Skeleton-Java
60//!   ohne JVM-Build-Verifikation.
61//! * Keine JNI-Anbindung — `Requester`/`Replier` halten `Object`-Felder
62//!   fuer den Native-Handle (Stub-Form).
63//! * Keine Reflection-TypeRep (Stretch in idl4-java §8).
64
65extern crate alloc;
66
67use alloc::format;
68use alloc::string::{String, ToString};
69use alloc::vec::Vec;
70use core::fmt::Write;
71
72use zerodds_idl::ast::{IntegerType, PrimitiveType, ScopedName, TypeSpec};
73use zerodds_rpc::service_mapping::{MethodDef, ParamDef, ParamDirection, ServiceDef};
74
75use crate::JavaGenOptions;
76use crate::emitter::{JavaFile, fmt_err, indent_unit, wrap_compilation_unit_default};
77use crate::error::JavaGenError;
78use crate::keywords::sanitize_identifier;
79use crate::type_map::{
80    floating_to_java, floating_to_java_boxed, integer_to_java, integer_to_java_boxed,
81    primitive_to_java, primitive_to_java_boxed,
82};
83
84// ---------------------------------------------------------------------------
85// Public API — siehe Crate-Doc fuer Aufruf-Schema
86// ---------------------------------------------------------------------------
87
88/// Emittiert das synchrone Service-Interface (`<Service>.java`).
89///
90/// # Errors
91/// `JavaGenError::InvalidName` bei nicht-sanitisierbarem Service-Namen.
92pub fn emit_service_interface(
93    svc: &ServiceDef,
94    pkg: &str,
95    opts: &JavaGenOptions,
96) -> Result<JavaFile, JavaGenError> {
97    let class = sanitize_identifier(&svc.name)?;
98    let ind = indent_unit(opts);
99    let mut body = String::new();
100    writeln!(body, "/** Synchronous service interface for {class}. */").map_err(fmt_err)?;
101    writeln!(body, "@org.zerodds.rpc.Service(\"{}\")", svc.name).map_err(fmt_err)?;
102    writeln!(body, "public interface {class} {{").map_err(fmt_err)?;
103    for m in &svc.methods {
104        emit_sync_method_signature(&mut body, m, &ind)?;
105    }
106    writeln!(body, "}}").map_err(fmt_err)?;
107    let source = wrap_compilation_unit_default(pkg, &body);
108    Ok(JavaFile {
109        package_path: pkg.to_string(),
110        class_name: class,
111        source,
112    })
113}
114
115/// Emittiert das asynchrone Service-Interface (`<Service>Async.java`).
116///
117/// # Errors
118/// Wie [`emit_service_interface`].
119pub fn emit_service_interface_async(
120    svc: &ServiceDef,
121    pkg: &str,
122    opts: &JavaGenOptions,
123) -> Result<JavaFile, JavaGenError> {
124    let svc_class = sanitize_identifier(&svc.name)?;
125    let class = format!("{svc_class}Async");
126    let ind = indent_unit(opts);
127    let mut body = String::new();
128    writeln!(
129        body,
130        "/** Asynchronous service interface for {svc_class}. */"
131    )
132    .map_err(fmt_err)?;
133    writeln!(body, "public interface {class} {{").map_err(fmt_err)?;
134    for m in &svc.methods {
135        emit_async_method_signature(&mut body, m, &ind)?;
136    }
137    writeln!(body, "}}").map_err(fmt_err)?;
138    let source = wrap_compilation_unit_default(pkg, &body);
139    Ok(JavaFile {
140        package_path: pkg.to_string(),
141        class_name: class,
142        source,
143    })
144}
145
146/// Emittiert die Client-seitige Requester-Klasse (`<Service>Requester.java`).
147///
148/// # Errors
149/// Wie [`emit_service_interface`].
150pub fn emit_requester_class(
151    svc: &ServiceDef,
152    pkg: &str,
153    opts: &JavaGenOptions,
154) -> Result<JavaFile, JavaGenError> {
155    let svc_class = sanitize_identifier(&svc.name)?;
156    let class = format!("{svc_class}Requester");
157    let ind = indent_unit(opts);
158    let mut body = String::new();
159    writeln!(
160        body,
161        "/** Client-side proxy for {svc_class}. Implements both the \
162         synchronous {svc_class} interface and the {svc_class}Async \
163         interface. */",
164    )
165    .map_err(fmt_err)?;
166    writeln!(
167        body,
168        "public final class {class} implements {svc_class}, {svc_class}Async {{",
169    )
170    .map_err(fmt_err)?;
171    if cfg!(feature = "jni") {
172        // JNI-Variante: wir delegieren direkt an `RustRequesterFFI` und
173        // halten den native Handle dort.
174        writeln!(
175            body,
176            "{ind}private final org.zerodds.rpc.RustRequesterFFI requesterFfi;",
177        )
178        .map_err(fmt_err)?;
179        writeln!(body).map_err(fmt_err)?;
180        writeln!(
181            body,
182            "{ind}public {class}(org.zerodds.rpc.RustRequesterFFI requesterFfi) {{",
183        )
184        .map_err(fmt_err)?;
185        writeln!(body, "{ind}{ind}this.requesterFfi = requesterFfi;").map_err(fmt_err)?;
186        writeln!(body, "{ind}}}").map_err(fmt_err)?;
187        writeln!(body).map_err(fmt_err)?;
188    } else {
189        writeln!(
190            body,
191            "{ind}private final org.zerodds.rpc.Requester<Object, Object> requester;",
192        )
193        .map_err(fmt_err)?;
194        writeln!(body).map_err(fmt_err)?;
195        writeln!(
196            body,
197            "{ind}public {class}(org.zerodds.rpc.Requester<Object, Object> requester) {{",
198        )
199        .map_err(fmt_err)?;
200        writeln!(body, "{ind}{ind}this.requester = requester;").map_err(fmt_err)?;
201        writeln!(body, "{ind}}}").map_err(fmt_err)?;
202        writeln!(body).map_err(fmt_err)?;
203    }
204
205    // Sync-Methoden: rufen die Async-Variante auf und blocken.
206    for m in &svc.methods {
207        emit_requester_sync_impl(&mut body, m, &ind)?;
208    }
209    writeln!(body).map_err(fmt_err)?;
210    // Async-Methoden: senden Request, geben CompletableFuture zurueck.
211    for m in &svc.methods {
212        emit_requester_async_impl(&mut body, m, &ind)?;
213    }
214
215    writeln!(body, "}}").map_err(fmt_err)?;
216    let source = wrap_compilation_unit_default(pkg, &body);
217    Ok(JavaFile {
218        package_path: pkg.to_string(),
219        class_name: class,
220        source,
221    })
222}
223
224/// Emittiert die Server-seitige Replier-Klasse (`<Service>Replier.java`).
225///
226/// # Errors
227/// Wie [`emit_service_interface`].
228pub fn emit_replier_class(
229    svc: &ServiceDef,
230    pkg: &str,
231    opts: &JavaGenOptions,
232) -> Result<JavaFile, JavaGenError> {
233    let svc_class = sanitize_identifier(&svc.name)?;
234    let class = format!("{svc_class}Replier");
235    let handler_iface = format!("{svc_class}Service");
236    let ind = indent_unit(opts);
237    let mut body = String::new();
238    writeln!(
239        body,
240        "/** Server-side replier for {svc_class}. Wires a {handler_iface} \
241         implementation to the underlying RPC runtime. */",
242    )
243    .map_err(fmt_err)?;
244    writeln!(body, "public final class {class} {{").map_err(fmt_err)?;
245    writeln!(
246        body,
247        "{ind}private final org.zerodds.rpc.Replier<Object, Object> replier;",
248    )
249    .map_err(fmt_err)?;
250    writeln!(body, "{ind}private final {handler_iface} handler;").map_err(fmt_err)?;
251    writeln!(body).map_err(fmt_err)?;
252    writeln!(
253        body,
254        "{ind}public {class}(org.zerodds.rpc.Replier<Object, Object> replier, {handler_iface} handler) {{",
255    )
256    .map_err(fmt_err)?;
257    writeln!(body, "{ind}{ind}this.replier = replier;").map_err(fmt_err)?;
258    writeln!(body, "{ind}{ind}this.handler = handler;").map_err(fmt_err)?;
259    writeln!(body, "{ind}}}").map_err(fmt_err)?;
260    writeln!(body).map_err(fmt_err)?;
261
262    // Dispatch-Stub: nimmt eine Request entgegen und ruft die richtige
263    // Handler-Methode anhand der Method-ID auf.
264    writeln!(
265        body,
266        "{ind}/** Dispatches an incoming request by method id. */",
267    )
268    .map_err(fmt_err)?;
269    writeln!(
270        body,
271        "{ind}public Object dispatch(int methodId, Object args) {{",
272    )
273    .map_err(fmt_err)?;
274    writeln!(body, "{ind}{ind}switch (methodId) {{").map_err(fmt_err)?;
275    for (idx, m) in svc.methods.iter().enumerate() {
276        let mname = sanitize_identifier(&m.name)?;
277        let case_id = idx + 1;
278        // void-return (inkl. oneway) → kein `return` vor dem Aufruf;
279        // sonst Result als `return`-Wert weiterreichen.
280        let void_like = m.oneway
281            || (m.return_type.is_none()
282                && m.params.iter().all(|p| p.direction == ParamDirection::In));
283        let stub = if void_like {
284            format!("{ind}{ind}{ind}case {case_id}: handler.{mname}(/* args */); return null;")
285        } else {
286            format!("{ind}{ind}{ind}case {case_id}: return handler.{mname}(/* args */);")
287        };
288        writeln!(body, "{stub}").map_err(fmt_err)?;
289    }
290    writeln!(
291        body,
292        "{ind}{ind}{ind}default: throw new org.zerodds.rpc.RemoteException(\
293         \"unknown method id: \" + methodId, \
294         org.zerodds.rpc.RemoteExceptionCode.UNKNOWN_OPERATION);",
295    )
296    .map_err(fmt_err)?;
297    writeln!(body, "{ind}{ind}}}").map_err(fmt_err)?;
298    writeln!(body, "{ind}}}").map_err(fmt_err)?;
299
300    writeln!(body, "}}").map_err(fmt_err)?;
301    let source = wrap_compilation_unit_default(pkg, &body);
302    Ok(JavaFile {
303        package_path: pkg.to_string(),
304        class_name: class,
305        source,
306    })
307}
308
309/// Emittiert das Server-side Handler-Interface (`<Service>Service.java`).
310///
311/// Anwender implementieren dieses Interface und uebergeben ihre
312/// Implementierung dem [`emit_replier_class`]-Output.
313///
314/// # Errors
315/// Wie [`emit_service_interface`].
316pub fn emit_service_handler_interface(
317    svc: &ServiceDef,
318    pkg: &str,
319    opts: &JavaGenOptions,
320) -> Result<JavaFile, JavaGenError> {
321    let svc_class = sanitize_identifier(&svc.name)?;
322    let class = format!("{svc_class}Service");
323    let ind = indent_unit(opts);
324    let mut body = String::new();
325    writeln!(
326        body,
327        "/** Server-side handler interface for {svc_class}. Implementors \
328         provide the actual business logic; a {svc_class}Replier wires \
329         them to the RPC runtime. */",
330    )
331    .map_err(fmt_err)?;
332    writeln!(body, "public interface {class} {{").map_err(fmt_err)?;
333    for m in &svc.methods {
334        emit_handler_method_signature(&mut body, m, &ind)?;
335    }
336    writeln!(body, "}}").map_err(fmt_err)?;
337    let source = wrap_compilation_unit_default(pkg, &body);
338    Ok(JavaFile {
339        package_path: pkg.to_string(),
340        class_name: class,
341        source,
342    })
343}
344
345/// Convenience-Wrapper: emittiert alle 5 Java-Files fuer ein Service.
346///
347/// Reihenfolge: Interface, Async-Interface, Handler-Interface, Requester,
348/// Replier.
349///
350/// # Errors
351/// Wie [`emit_service_interface`].
352pub fn emit_service_files(
353    svc: &ServiceDef,
354    pkg: &str,
355    opts: &JavaGenOptions,
356) -> Result<Vec<JavaFile>, JavaGenError> {
357    Ok(alloc::vec![
358        emit_service_interface(svc, pkg, opts)?,
359        emit_service_interface_async(svc, pkg, opts)?,
360        emit_service_handler_interface(svc, pkg, opts)?,
361        emit_requester_class(svc, pkg, opts)?,
362        emit_replier_class(svc, pkg, opts)?,
363    ])
364}
365
366// ---------------------------------------------------------------------------
367// Method-Signature-Emitter
368// ---------------------------------------------------------------------------
369
370fn emit_sync_method_signature(
371    out: &mut String,
372    m: &MethodDef,
373    ind: &str,
374) -> Result<(), JavaGenError> {
375    let name = sanitize_identifier(&m.name)?;
376    let ret = sync_return_type(m)?;
377    let params = render_method_params(m)?;
378    let throws = String::new();
379    if m.oneway {
380        writeln!(out, "{ind}@org.zerodds.rpc.Oneway").map_err(fmt_err)?;
381    }
382    writeln!(out, "{ind}{ret} {name}({params}){throws};").map_err(fmt_err)?;
383    Ok(())
384}
385
386fn emit_async_method_signature(
387    out: &mut String,
388    m: &MethodDef,
389    ind: &str,
390) -> Result<(), JavaGenError> {
391    let name = sanitize_identifier(&m.name)?;
392    let async_name = format!("{name}Async");
393    let ret = async_return_type(m)?;
394    let params = render_method_params_async(m)?;
395    if m.oneway {
396        writeln!(out, "{ind}@org.zerodds.rpc.Oneway").map_err(fmt_err)?;
397    }
398    writeln!(out, "{ind}{ret} {async_name}({params});").map_err(fmt_err)?;
399    Ok(())
400}
401
402fn emit_handler_method_signature(
403    out: &mut String,
404    m: &MethodDef,
405    ind: &str,
406) -> Result<(), JavaGenError> {
407    // Handler-Signatur ist identisch zur sync-Signatur — der
408    // Replier-Dispatch ruft sie direkt auf.
409    emit_sync_method_signature(out, m, ind)
410}
411
412fn emit_requester_sync_impl(
413    out: &mut String,
414    m: &MethodDef,
415    ind: &str,
416) -> Result<(), JavaGenError> {
417    let name = sanitize_identifier(&m.name)?;
418    let async_name = format!("{name}Async");
419    let ret_ty = sync_return_type(m)?;
420    let params = render_method_params(m)?;
421    let arg_list = render_call_arglist(m)?;
422    writeln!(out, "{ind}@Override").map_err(fmt_err)?;
423    writeln!(out, "{ind}public {ret_ty} {name}({params}) {{").map_err(fmt_err)?;
424    if m.oneway {
425        writeln!(out, "{ind}{ind}{async_name}({arg_list});").map_err(fmt_err)?;
426        writeln!(out, "{ind}}}").map_err(fmt_err)?;
427        return Ok(());
428    }
429    if m.return_type.is_none() && m.params.iter().all(|p| p.direction == ParamDirection::In) {
430        writeln!(
431            out,
432            "{ind}{ind}try {{ {async_name}({arg_list}).get(); }} catch (Exception e) {{ \
433             throw new org.zerodds.rpc.RemoteException(e); }}",
434        )
435        .map_err(fmt_err)?;
436    } else {
437        writeln!(
438            out,
439            "{ind}{ind}try {{ return {async_name}({arg_list}).get(); }} catch (Exception e) {{ \
440             throw new org.zerodds.rpc.RemoteException(e); }}",
441        )
442        .map_err(fmt_err)?;
443    }
444    writeln!(out, "{ind}}}").map_err(fmt_err)?;
445    Ok(())
446}
447
448fn emit_requester_async_impl(
449    out: &mut String,
450    m: &MethodDef,
451    ind: &str,
452) -> Result<(), JavaGenError> {
453    let name = sanitize_identifier(&m.name)?;
454    let async_name = format!("{name}Async");
455    let ret_ty = async_return_type(m)?;
456    let params = render_method_params_async(m)?;
457    writeln!(out, "{ind}@Override").map_err(fmt_err)?;
458    writeln!(out, "{ind}public {ret_ty} {async_name}({params}) {{").map_err(fmt_err)?;
459
460    // JNI-Skeleton: Wenn das `jni`-Feature aktiv ist, zeigen wir
461    // direkt den Rust-FFI-Pfad. Der Marshalling-Code ist bewusst
462    // als Kommentar markiert — pro Service waere ein dedizierter
463    // XCDR2-Encoder noetig, der in einer spaeteren Codegen-Stufe
464    // emittiert wird (idl4-java §8 / zerodds-rpc §7.11).
465    if cfg!(feature = "jni") {
466        if m.oneway {
467            writeln!(
468                out,
469                "{ind}{ind}// JNI: oneway -> Requester.sendRequest blocking + ignore reply.",
470            )
471            .map_err(fmt_err)?;
472            writeln!(
473                out,
474                "{ind}{ind}requesterFfi.sendRequest(/* xcdr2_encode(args) */ new byte[0], 0L);",
475            )
476            .map_err(fmt_err)?;
477            writeln!(
478                out,
479                "{ind}{ind}return java.util.concurrent.CompletableFuture.completedFuture(null);",
480            )
481            .map_err(fmt_err)?;
482        } else {
483            writeln!(
484                out,
485                "{ind}{ind}// JNI: async-Path via RustRequesterFFI.sendRequestAsync.",
486            )
487            .map_err(fmt_err)?;
488            writeln!(
489                out,
490                "{ind}{ind}return requesterFfi.sendRequestAsync(/* xcdr2_encode(args) */ new byte[0])",
491            )
492            .map_err(fmt_err)?;
493            writeln!(
494                out,
495                "{ind}{ind}{ind}.thenApply(reply -> /* xcdr2_decode<TOut>(reply) */ null);",
496            )
497            .map_err(fmt_err)?;
498        }
499        writeln!(out, "{ind}}}").map_err(fmt_err)?;
500        return Ok(());
501    }
502
503    if m.oneway {
504        writeln!(
505            out,
506            "{ind}{ind}requester.sendOneway(new Object[] {{ /* args */ }});",
507        )
508        .map_err(fmt_err)?;
509        writeln!(
510            out,
511            "{ind}{ind}return java.util.concurrent.CompletableFuture.completedFuture(null);",
512        )
513        .map_err(fmt_err)?;
514    } else {
515        writeln!(
516            out,
517            "{ind}{ind}return requester.sendRequest(new Object[] {{ /* args */ }});",
518        )
519        .map_err(fmt_err)?;
520    }
521    writeln!(out, "{ind}}}").map_err(fmt_err)?;
522    Ok(())
523}
524
525// ---------------------------------------------------------------------------
526// Type-Mapping-Helpers
527// ---------------------------------------------------------------------------
528
529/// Liefert den Java-Return-Type fuer die synchrone Methoden-Signatur.
530/// `oneway` und `void` → `void`. `out`-Parameter sind im Holder-Pattern
531/// abgebildet — sie bleiben Teil der Parameterliste.
532fn sync_return_type(m: &MethodDef) -> Result<String, JavaGenError> {
533    if m.oneway {
534        return Ok("void".to_string());
535    }
536    match &m.return_type {
537        None => Ok("void".to_string()),
538        Some(ts) => typespec_to_java_unboxed(ts),
539    }
540}
541
542/// Liefert den Java-Async-Return-Type. `oneway` ergibt
543/// `CompletableFuture<Void>` (Caller darf trotzdem auf "complete"
544/// warten, auch wenn Reply ueberspringen wird), `void` → `Void`,
545/// sonst `CompletableFuture<TBoxed>`.
546fn async_return_type(m: &MethodDef) -> Result<String, JavaGenError> {
547    if m.oneway {
548        return Ok("java.util.concurrent.CompletableFuture<Void>".to_string());
549    }
550    match &m.return_type {
551        None => Ok("java.util.concurrent.CompletableFuture<Void>".to_string()),
552        Some(ts) => Ok(format!(
553            "java.util.concurrent.CompletableFuture<{}>",
554            typespec_to_java_boxed(ts)?
555        )),
556    }
557}
558
559/// Param-Liste fuer die sync-Variante. `out`/`inout` werden als
560/// Holder gerendert.
561fn render_method_params(m: &MethodDef) -> Result<String, JavaGenError> {
562    let mut parts: Vec<String> = Vec::new();
563    for p in &m.params {
564        parts.push(render_param(p)?);
565    }
566    Ok(parts.join(", "))
567}
568
569/// Param-Liste fuer die async-Variante; Holder-Pattern bleibt erhalten.
570fn render_method_params_async(m: &MethodDef) -> Result<String, JavaGenError> {
571    render_method_params(m)
572}
573
574fn render_param(p: &ParamDef) -> Result<String, JavaGenError> {
575    let name = sanitize_identifier(&p.name)?;
576    let ty = match p.direction {
577        ParamDirection::In => typespec_to_java_unboxed(&p.type_ref)?,
578        ParamDirection::Out | ParamDirection::InOut => {
579            // Holder-Pattern (Spec §7.11.2 / IDL2Java 1.3 §1.5).
580            format!(
581                "org.zerodds.rpc.Holder<{}>",
582                typespec_to_java_boxed(&p.type_ref)?
583            )
584        }
585    };
586    Ok(format!("{ty} {name}"))
587}
588
589fn render_call_arglist(m: &MethodDef) -> Result<String, JavaGenError> {
590    let mut parts: Vec<String> = Vec::new();
591    for p in &m.params {
592        parts.push(sanitize_identifier(&p.name)?);
593    }
594    Ok(parts.join(", "))
595}
596
597// ---------------------------------------------------------------------------
598// TypeSpec → Java
599// ---------------------------------------------------------------------------
600
601fn typespec_to_java_unboxed(ts: &TypeSpec) -> Result<String, JavaGenError> {
602    match ts {
603        TypeSpec::Primitive(p) => Ok(primitive_to_java(*p).to_string()),
604        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
605        TypeSpec::String(_) => Ok("String".to_string()),
606        TypeSpec::Sequence(s) => Ok(format!(
607            "java.util.List<{}>",
608            typespec_to_java_boxed(&s.elem)?
609        )),
610        TypeSpec::Map(mm) => Ok(format!(
611            "java.util.Map<{}, {}>",
612            typespec_to_java_boxed(&mm.key)?,
613            typespec_to_java_boxed(&mm.value)?,
614        )),
615        TypeSpec::Fixed(_) => Err(JavaGenError::UnsupportedConstruct {
616            construct: "fixed".into(),
617            context: None,
618        }),
619        TypeSpec::Any => Err(JavaGenError::UnsupportedConstruct {
620            construct: "any".into(),
621            context: None,
622        }),
623    }
624}
625
626fn typespec_to_java_boxed(ts: &TypeSpec) -> Result<String, JavaGenError> {
627    match ts {
628        TypeSpec::Primitive(p) => Ok(primitive_to_java_boxed(*p).to_string()),
629        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
630        TypeSpec::String(_) => Ok("String".to_string()),
631        TypeSpec::Sequence(s) => Ok(format!(
632            "java.util.List<{}>",
633            typespec_to_java_boxed(&s.elem)?
634        )),
635        TypeSpec::Map(mm) => Ok(format!(
636            "java.util.Map<{}, {}>",
637            typespec_to_java_boxed(&mm.key)?,
638            typespec_to_java_boxed(&mm.value)?,
639        )),
640        TypeSpec::Fixed(_) => Err(JavaGenError::UnsupportedConstruct {
641            construct: "fixed".into(),
642            context: None,
643        }),
644        TypeSpec::Any => Err(JavaGenError::UnsupportedConstruct {
645            construct: "any".into(),
646            context: None,
647        }),
648    }
649}
650
651fn scoped_to_java(s: &ScopedName) -> String {
652    s.parts
653        .iter()
654        .map(|p| p.text.clone())
655        .collect::<Vec<_>>()
656        .join(".")
657}
658
659// Marker, damit der Linter die "vielleicht-spaeter"-Helpers nicht
660// als unused meldet.
661#[allow(dead_code)]
662fn _unused_marker(_i: IntegerType) {
663    let _ = integer_to_java;
664    let _ = floating_to_java;
665    let _ = integer_to_java_boxed;
666    let _ = floating_to_java_boxed;
667    let _ = PrimitiveType::Boolean;
668}