Skip to main content

zerodds_rpc/
service_mapping.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Service-IDL → typisiertes Service-Datenmodell — Spec §7.4.
5//!
6//! Die DDS-RPC-Spec mappt eine IDL-Service-Definition
7//!
8//! ```text
9//! @service interface Calculator {
10//!     long add(in long a, in long b);
11//!     oneway void log(in string msg);
12//! };
13//! ```
14//!
15//! auf zwei Wire-Strukturen pro Methode:
16//!
17//! * `<Service>_<Method>_In`  — Request-Payload (alle `in`/`inout`).
18//! * `<Service>_<Method>_Out` — Reply-Payload (Return + `out`/`inout`).
19//!
20//! Diese Foundation-Stufe (C6.1.A) stellt nur das **Datenmodell** dar
21//! ([`ServiceDef`], [`MethodDef`], [`ParamDef`]). Die eigentliche
22//! Codegen-Stufe (C6.1.B) konsumiert das Modell und emittiert IDL-
23//! Strukturen + Rust-Bindings.
24//!
25//! Das Modell wird per [`lower_service`] aus einem `zerodds_idl::ast::
26//! InterfaceDef` plus den bereits typisierten RPC-Annotations
27//! ([`crate::annotations::LoweredRpc`]) konstruiert. Validierung in
28//! dieser Stufe:
29//!
30//! * Service-Name ist nicht-leer + alphanumerisch + `_`.
31//! * Methoden-Namen sind eindeutig.
32//! * Parameter-Namen pro Methode sind eindeutig.
33//! * `oneway`-Methoden haben `void`-Return und keine `out`/`inout`-Params.
34
35extern crate alloc;
36
37use alloc::string::{String, ToString};
38use alloc::vec::Vec;
39
40use zerodds_idl::ast::{Export, InterfaceDef, OpDecl, ParamAttribute, TypeSpec};
41
42use crate::annotations::{LoweredRpc, lower_rpc_annotations};
43use crate::error::{RpcError, RpcResult};
44use crate::topic_naming::{ServiceTopicNames, validate_service_name};
45
46/// Direction-Attribut eines RPC-Parameters.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ParamDirection {
49    /// `in` — Request-only.
50    In,
51    /// `out` — Reply-only.
52    Out,
53    /// `inout` — beides.
54    InOut,
55}
56
57impl From<ParamAttribute> for ParamDirection {
58    fn from(value: ParamAttribute) -> Self {
59        match value {
60            ParamAttribute::In => Self::In,
61            ParamAttribute::Out => Self::Out,
62            ParamAttribute::InOut => Self::InOut,
63        }
64    }
65}
66
67impl ParamDirection {
68    /// `true` wenn Parameter im Request-Topic landet (`in` oder `inout`).
69    #[must_use]
70    pub const fn is_in(self) -> bool {
71        matches!(self, Self::In | Self::InOut)
72    }
73
74    /// `true` wenn Parameter im Reply-Topic landet (`out` oder `inout`).
75    #[must_use]
76    pub const fn is_out(self) -> bool {
77        matches!(self, Self::Out | Self::InOut)
78    }
79}
80
81/// Type-Referenz im Service-Modell. In Foundation-Stufe ist das nur ein
82/// Re-Use des AST-`TypeSpec` — Phase C6.1.B kann hier ein resolviertes
83/// Type-Objekt einsetzen.
84pub type TypeRef = TypeSpec;
85
86/// Ein RPC-Parameter.
87#[derive(Debug, Clone, PartialEq)]
88pub struct ParamDef {
89    /// Parameter-Name.
90    pub name: String,
91    /// Direction (`in`/`out`/`inout`).
92    pub direction: ParamDirection,
93    /// Typ.
94    pub type_ref: TypeRef,
95}
96
97/// Eine RPC-Methode.
98#[derive(Debug, Clone, PartialEq)]
99pub struct MethodDef {
100    /// Methoden-Name.
101    pub name: String,
102    /// Parameter in Deklarations-Reihenfolge.
103    pub params: Vec<ParamDef>,
104    /// Return-Type. `None` bei `void`.
105    pub return_type: Option<TypeRef>,
106    /// `true` wenn `oneway` (kein Reply, nur `in`-Params, void-Return).
107    pub oneway: bool,
108}
109
110impl MethodDef {
111    /// Alle `in`/`inout`-Parameter (Request-Felder).
112    pub fn in_params(&self) -> impl Iterator<Item = &ParamDef> {
113        self.params.iter().filter(|p| p.direction.is_in())
114    }
115
116    /// Alle `out`/`inout`-Parameter (Reply-Felder).
117    pub fn out_params(&self) -> impl Iterator<Item = &ParamDef> {
118        self.params.iter().filter(|p| p.direction.is_out())
119    }
120}
121
122/// Eine RPC-Service-Definition.
123#[derive(Debug, Clone, PartialEq)]
124pub struct ServiceDef {
125    /// Effektiver Service-Name (`@service(name="...")` oder Interface-Name).
126    pub name: String,
127    /// Methoden.
128    pub methods: Vec<MethodDef>,
129}
130
131impl ServiceDef {
132    /// Liefert die zugehoerigen Topic-Namen (`<svc>_Request` / `<svc>_Reply`).
133    ///
134    /// # Errors
135    /// Sollte nicht passieren — `lower_service` validiert den Namen
136    /// bereits. Defensiv trotzdem propagiert.
137    pub fn topic_names(&self) -> RpcResult<ServiceTopicNames> {
138        ServiceTopicNames::new(&self.name)
139    }
140}
141
142/// Lowert eine IDL-Service-Definition in das typisierte Service-Modell.
143///
144/// Erwartet, dass die Annotations des Interface bereits per
145/// [`lower_rpc_annotations`] vorgelowert wurden. Wenn `@service` nicht
146/// gesetzt ist, wird der Interface-Name als Service-Name benutzt.
147///
148/// # Errors
149/// * `RpcError::InvalidServiceName` bei leerem oder ungueltigem Namen.
150/// * `RpcError::InvalidMethodName` bei leerem Methoden-Namen.
151/// * `RpcError::OnewayWithReturn` / `OnewayWithOutParam` bei
152///   inkonsistenten oneway-Deklarationen.
153/// * `RpcError::DuplicateMethod` / `DuplicateParam` bei Namens-Kollisionen.
154pub fn lower_service(iface: &InterfaceDef, lowered: &LoweredRpc) -> RpcResult<ServiceDef> {
155    // Service-Name aus @service(name="...") oder Interface-Name.
156    let svc_name = lowered
157        .service_name()
158        .map(ToString::to_string)
159        .unwrap_or_else(|| iface.name.text.clone());
160    validate_service_name(&svc_name)?;
161
162    let mut methods = Vec::new();
163    for export in &iface.exports {
164        if let Export::Op(op) = export {
165            methods.push(lower_method(op)?);
166        }
167        // Andere Exports (Attr/Type/Const/Except) sind in C6.1.A explizit
168        // out-of-scope — exceptions werden in C6.1.B als RemoteException-
169        // Carrier modelliert.
170    }
171
172    // Duplicate-Method-Check.
173    for i in 0..methods.len() {
174        for j in (i + 1)..methods.len() {
175            if methods[i].name == methods[j].name {
176                return Err(RpcError::DuplicateMethod(methods[i].name.clone()));
177            }
178        }
179    }
180
181    Ok(ServiceDef {
182        name: svc_name,
183        methods,
184    })
185}
186
187fn lower_method(op: &OpDecl) -> RpcResult<MethodDef> {
188    let name = op.name.text.clone();
189    if name.is_empty() {
190        return Err(RpcError::InvalidMethodName(name));
191    }
192
193    // Annotation-Lowering der Methoden-Annotations: `@oneway` als
194    // Annotation-Form muss aequivalent zum AST-`oneway`-Keyword
195    // behandelt werden.
196    let method_anns = lower_rpc_annotations(&op.annotations);
197    let oneway = op.oneway || method_anns.has_oneway();
198
199    let return_type = op.return_type.clone();
200
201    if oneway && return_type.is_some() {
202        return Err(RpcError::OnewayWithReturn(name));
203    }
204
205    let mut params = Vec::with_capacity(op.params.len());
206    for p in &op.params {
207        // Param-Annotation `@in/@out/@inout` ueberschreibt das native
208        // ParamAttribute, falls explizit gesetzt.
209        let p_anns = lower_rpc_annotations(&p.annotations);
210        let direction = override_direction(p.attribute, &p_anns);
211
212        if oneway && direction.is_out() {
213            return Err(RpcError::OnewayWithOutParam {
214                method: name.clone(),
215                param: p.name.text.clone(),
216            });
217        }
218
219        params.push(ParamDef {
220            name: p.name.text.clone(),
221            direction,
222            type_ref: p.type_spec.clone(),
223        });
224    }
225
226    // Duplicate-Param-Check.
227    for i in 0..params.len() {
228        for j in (i + 1)..params.len() {
229            if params[i].name == params[j].name {
230                return Err(RpcError::DuplicateParam {
231                    method: name,
232                    param: params[i].name.clone(),
233                });
234            }
235        }
236    }
237
238    Ok(MethodDef {
239        name,
240        params,
241        return_type,
242        oneway,
243    })
244}
245
246fn override_direction(native: ParamAttribute, anns: &LoweredRpc) -> ParamDirection {
247    use crate::annotations::RpcAnnotation;
248    for a in &anns.builtins {
249        match a {
250            RpcAnnotation::In => return ParamDirection::In,
251            RpcAnnotation::Out => return ParamDirection::Out,
252            RpcAnnotation::InOut => return ParamDirection::InOut,
253            _ => {}
254        }
255    }
256    native.into()
257}
258
259#[cfg(test)]
260#[allow(clippy::unwrap_used, clippy::expect_used)]
261mod tests {
262    use super::*;
263    use zerodds_idl::ast::{
264        Annotation, AnnotationParams, Identifier, IntegerType, InterfaceKind, OpDecl, ParamDecl,
265        PrimitiveType, ScopedName, StringType, TypeSpec,
266    };
267    use zerodds_idl::errors::Span;
268
269    fn sp() -> Span {
270        Span::SYNTHETIC
271    }
272
273    fn ident(t: &str) -> Identifier {
274        Identifier::new(t, sp())
275    }
276
277    fn long_t() -> TypeSpec {
278        TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long))
279    }
280
281    fn string_t() -> TypeSpec {
282        TypeSpec::String(StringType {
283            wide: false,
284            bound: None,
285            span: sp(),
286        })
287    }
288
289    fn op(
290        name: &str,
291        oneway: bool,
292        ret: Option<TypeSpec>,
293        params: Vec<ParamDecl>,
294        anns: Vec<Annotation>,
295    ) -> OpDecl {
296        OpDecl {
297            name: ident(name),
298            oneway,
299            return_type: ret,
300            params,
301            raises: Vec::new(),
302            annotations: anns,
303            span: sp(),
304        }
305    }
306
307    fn param(name: &str, attr: ParamAttribute, ty: TypeSpec) -> ParamDecl {
308        ParamDecl {
309            attribute: attr,
310            type_spec: ty,
311            name: ident(name),
312            annotations: Vec::new(),
313            span: sp(),
314        }
315    }
316
317    fn iface(name: &str, exports: Vec<Export>, anns: Vec<Annotation>) -> InterfaceDef {
318        InterfaceDef {
319            kind: InterfaceKind::Plain,
320            name: ident(name),
321            bases: Vec::new(),
322            exports,
323            annotations: anns,
324            span: sp(),
325        }
326    }
327
328    fn ann_simple(name: &str) -> Annotation {
329        Annotation {
330            name: ScopedName {
331                absolute: false,
332                parts: vec![ident(name)],
333                span: sp(),
334            },
335            params: AnnotationParams::None,
336            span: sp(),
337        }
338    }
339
340    #[test]
341    fn calculator_service_with_in_params_lowers() {
342        let add = op(
343            "add",
344            false,
345            Some(long_t()),
346            vec![
347                param("a", ParamAttribute::In, long_t()),
348                param("b", ParamAttribute::In, long_t()),
349            ],
350            Vec::new(),
351        );
352        let i = iface(
353            "Calculator",
354            vec![Export::Op(add)],
355            vec![ann_simple("service")],
356        );
357        let lowered = lower_rpc_annotations(&i.annotations);
358        let svc = lower_service(&i, &lowered).unwrap();
359        assert_eq!(svc.name, "Calculator");
360        assert_eq!(svc.methods.len(), 1);
361        let m = &svc.methods[0];
362        assert_eq!(m.name, "add");
363        assert!(!m.oneway);
364        assert_eq!(m.params.len(), 2);
365        assert_eq!(m.in_params().count(), 2);
366        assert_eq!(m.out_params().count(), 0);
367        assert_eq!(svc.topic_names().unwrap().request, "Calculator_Request");
368    }
369
370    #[test]
371    fn oneway_method_with_return_is_error() {
372        let bad = op(
373            "log",
374            true,
375            Some(long_t()),
376            vec![param("msg", ParamAttribute::In, string_t())],
377            Vec::new(),
378        );
379        let i = iface("Logger", vec![Export::Op(bad)], Vec::new());
380        let lowered = lower_rpc_annotations(&i.annotations);
381        let err = lower_service(&i, &lowered).unwrap_err();
382        assert!(matches!(err, RpcError::OnewayWithReturn(_)));
383    }
384
385    #[test]
386    fn oneway_method_with_out_param_is_error() {
387        let bad = op(
388            "fire",
389            true,
390            None,
391            vec![param("result", ParamAttribute::Out, long_t())],
392            Vec::new(),
393        );
394        let i = iface("Svc", vec![Export::Op(bad)], Vec::new());
395        let lowered = lower_rpc_annotations(&i.annotations);
396        let err = lower_service(&i, &lowered).unwrap_err();
397        assert!(matches!(err, RpcError::OnewayWithOutParam { .. }));
398    }
399
400    #[test]
401    fn oneway_method_with_inout_param_is_error() {
402        let bad = op(
403            "fire",
404            true,
405            None,
406            vec![param("v", ParamAttribute::InOut, long_t())],
407            Vec::new(),
408        );
409        let i = iface("Svc", vec![Export::Op(bad)], Vec::new());
410        let lowered = lower_rpc_annotations(&i.annotations);
411        let err = lower_service(&i, &lowered).unwrap_err();
412        assert!(matches!(err, RpcError::OnewayWithOutParam { .. }));
413    }
414
415    #[test]
416    fn oneway_with_only_in_params_lowers() {
417        let m = op(
418            "log",
419            true,
420            None,
421            vec![param("msg", ParamAttribute::In, string_t())],
422            Vec::new(),
423        );
424        let i = iface("Logger", vec![Export::Op(m)], Vec::new());
425        let lowered = lower_rpc_annotations(&i.annotations);
426        let svc = lower_service(&i, &lowered).unwrap();
427        assert!(svc.methods[0].oneway);
428        assert_eq!(svc.methods[0].in_params().count(), 1);
429        assert_eq!(svc.methods[0].out_params().count(), 0);
430    }
431
432    #[test]
433    fn oneway_via_annotation_recognized() {
434        // Native oneway=false, aber @oneway-Annotation gesetzt.
435        let m = op("ping", false, None, Vec::new(), vec![ann_simple("oneway")]);
436        let i = iface("Svc", vec![Export::Op(m)], Vec::new());
437        let lowered = lower_rpc_annotations(&i.annotations);
438        let svc = lower_service(&i, &lowered).unwrap();
439        assert!(svc.methods[0].oneway);
440    }
441
442    #[test]
443    fn duplicate_method_detected() {
444        let m1 = op("foo", false, None, Vec::new(), Vec::new());
445        let m2 = op("foo", false, None, Vec::new(), Vec::new());
446        let i = iface("Svc", vec![Export::Op(m1), Export::Op(m2)], Vec::new());
447        let lowered = lower_rpc_annotations(&i.annotations);
448        let err = lower_service(&i, &lowered).unwrap_err();
449        assert_eq!(err, RpcError::DuplicateMethod("foo".into()));
450    }
451
452    #[test]
453    fn duplicate_param_detected() {
454        let m = op(
455            "add",
456            false,
457            Some(long_t()),
458            vec![
459                param("x", ParamAttribute::In, long_t()),
460                param("x", ParamAttribute::In, long_t()),
461            ],
462            Vec::new(),
463        );
464        let i = iface("Svc", vec![Export::Op(m)], Vec::new());
465        let lowered = lower_rpc_annotations(&i.annotations);
466        let err = lower_service(&i, &lowered).unwrap_err();
467        assert!(matches!(err, RpcError::DuplicateParam { .. }));
468    }
469
470    #[test]
471    fn empty_method_name_rejected() {
472        let m = op("", false, None, Vec::new(), Vec::new());
473        let i = iface("Svc", vec![Export::Op(m)], Vec::new());
474        let lowered = lower_rpc_annotations(&i.annotations);
475        let err = lower_service(&i, &lowered).unwrap_err();
476        assert!(matches!(err, RpcError::InvalidMethodName(_)));
477    }
478
479    #[test]
480    fn invalid_service_name_rejected() {
481        let i = iface("Bad-Name", Vec::new(), Vec::new());
482        let lowered = lower_rpc_annotations(&i.annotations);
483        let err = lower_service(&i, &lowered).unwrap_err();
484        assert!(matches!(err, RpcError::InvalidServiceName(_)));
485    }
486
487    #[test]
488    fn service_name_from_annotation_overrides_iface_name() {
489        // @service(name="OuterName") gewinnt ueber den Interface-Namen.
490        let i = iface(
491            "InternalIface",
492            Vec::new(),
493            vec![Annotation {
494                name: ScopedName {
495                    absolute: false,
496                    parts: vec![ident("service")],
497                    span: sp(),
498                },
499                params: AnnotationParams::Named(vec![zerodds_idl::ast::NamedParam {
500                    name: ident("name"),
501                    value: zerodds_idl::ast::ConstExpr::Literal(zerodds_idl::ast::Literal {
502                        kind: zerodds_idl::ast::LiteralKind::String,
503                        raw: "\"OuterName\"".into(),
504                        span: sp(),
505                    }),
506                    span: sp(),
507                }]),
508                span: sp(),
509            }],
510        );
511        let lowered = lower_rpc_annotations(&i.annotations);
512        let svc = lower_service(&i, &lowered).unwrap();
513        assert_eq!(svc.name, "OuterName");
514    }
515
516    #[test]
517    fn inout_param_appears_in_both_directions() {
518        let m = op(
519            "swap",
520            false,
521            None,
522            vec![param("v", ParamAttribute::InOut, long_t())],
523            Vec::new(),
524        );
525        let i = iface("Svc", vec![Export::Op(m)], Vec::new());
526        let lowered = lower_rpc_annotations(&i.annotations);
527        let svc = lower_service(&i, &lowered).unwrap();
528        let m = &svc.methods[0];
529        assert_eq!(m.in_params().count(), 1);
530        assert_eq!(m.out_params().count(), 1);
531    }
532
533    #[test]
534    fn out_only_param_is_reply_only() {
535        let m = op(
536            "result",
537            false,
538            None,
539            vec![param("v", ParamAttribute::Out, long_t())],
540            Vec::new(),
541        );
542        let i = iface("Svc", vec![Export::Op(m)], Vec::new());
543        let lowered = lower_rpc_annotations(&i.annotations);
544        let svc = lower_service(&i, &lowered).unwrap();
545        let m = &svc.methods[0];
546        assert_eq!(m.in_params().count(), 0);
547        assert_eq!(m.out_params().count(), 1);
548    }
549
550    #[test]
551    fn param_annotation_in_overrides_native_attr() {
552        // ParamAttribute::Out, aber @in-Annotation -> @in gewinnt.
553        let mut p = param("v", ParamAttribute::Out, long_t());
554        p.annotations.push(ann_simple("in"));
555        let m = op("foo", false, None, vec![p], Vec::new());
556        let i = iface("Svc", vec![Export::Op(m)], Vec::new());
557        let lowered = lower_rpc_annotations(&i.annotations);
558        let svc = lower_service(&i, &lowered).unwrap();
559        assert_eq!(svc.methods[0].params[0].direction, ParamDirection::In);
560    }
561
562    #[test]
563    fn non_op_exports_are_ignored() {
564        // Const-Exports sollen das Service-Modell nicht stoeren — sie
565        // werden in C6.1.A explizit nicht abgebildet.
566        let const_decl = zerodds_idl::ast::ConstDecl {
567            name: ident("MAX"),
568            type_: zerodds_idl::ast::ConstType::Integer(IntegerType::Long),
569            value: zerodds_idl::ast::ConstExpr::Literal(zerodds_idl::ast::Literal {
570                kind: zerodds_idl::ast::LiteralKind::Integer,
571                raw: "10".into(),
572                span: sp(),
573            }),
574            annotations: Vec::new(),
575            span: sp(),
576        };
577        let m = op("foo", false, None, Vec::new(), Vec::new());
578        let i = iface(
579            "Svc",
580            vec![Export::Const(const_decl), Export::Op(m)],
581            Vec::new(),
582        );
583        let lowered = lower_rpc_annotations(&i.annotations);
584        let svc = lower_service(&i, &lowered).unwrap();
585        assert_eq!(svc.methods.len(), 1);
586    }
587
588    #[test]
589    fn param_direction_helpers() {
590        assert!(ParamDirection::In.is_in());
591        assert!(!ParamDirection::In.is_out());
592        assert!(!ParamDirection::Out.is_in());
593        assert!(ParamDirection::Out.is_out());
594        assert!(ParamDirection::InOut.is_in());
595        assert!(ParamDirection::InOut.is_out());
596    }
597
598    #[test]
599    fn param_direction_from_param_attribute() {
600        assert_eq!(ParamDirection::from(ParamAttribute::In), ParamDirection::In);
601        assert_eq!(
602            ParamDirection::from(ParamAttribute::Out),
603            ParamDirection::Out
604        );
605        assert_eq!(
606            ParamDirection::from(ParamAttribute::InOut),
607            ParamDirection::InOut
608        );
609    }
610}