Skip to main content

zerodds_ccm/
lightweight.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Lightweight CCM Profile — Spec §13.
5//!
6//! Spec §13.1 (S. 273) — Summary: "The Lightweight CCM (LwCCM) profile
7//! is intended to be useful in environments where: a small footprint
8//! [...] CCM with reduced support for: Persistence, Introspection,
9//! Navigation, Type-specific Generic Operations, Segmentation,
10//! Transactions, Security, Configurators, Proxy Homes, Home Finders."
11//!
12//! Wir implementieren das als Filter-Funktion auf dem `ComponentEquivalent`
13//! / `HomeEquivalent` aus [`crate::transform`] — alle Operationen, die
14//! in §13.2-§13.10 explizit ausgeschlossen sind, werden entfernt.
15
16use alloc::vec::Vec;
17use core::fmt;
18
19use zerodds_idl::ast::{Export, InterfaceDef, ScopedName};
20
21use crate::transform::ComponentEquivalent;
22
23/// Filter-Fehler.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum LightweightFilterError {
26    /// Ein verlangter LwCCM-konformer Component wuerde nach Filter
27    /// keine Operations mehr haben — typischer Fall: der Component
28    /// hatte ausschliesslich Persistence-/Configurator-/Proxy-Home-
29    /// Features.
30    EmptyAfterFilter,
31}
32
33impl fmt::Display for LightweightFilterError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::EmptyAfterFilter => f.write_str(
37                "component would have no operations after applying lightweight CCM filter",
38            ),
39        }
40    }
41}
42
43#[cfg(feature = "std")]
44impl std::error::Error for LightweightFilterError {}
45
46/// Wendet das Lightweight-CCM-Profile §13 auf ein
47/// [`ComponentEquivalent`] an. Spec-Sections, die gefiltert werden:
48///
49/// * §13.3 (S. 276) — keine `provide_facet`/`get_all_facets`/
50///   `get_named_facets` (Navigation generic ops).
51/// * §13.3 (S. 276) — keine `connect`/`disconnect`/`get_connection(s)`
52///   GENERIC ops auf `Receptacles`-Iface (type-specific bleiben).
53/// * §13.3 (S. 276) — keine `subscribe`/`unsubscribe`/
54///   `connect_consumer`/`disconnect_consumer`/`get_consumer`/
55///   `get_all_consumers`/`get_named_consumers` GENERIC ops auf `Events`
56///   Interface (type-specific bleiben).
57/// * §13.7 (S. 279) — keine Configurator-Methoden (`configure`,
58///   `set_configuration`, `configuration_complete`).
59///
60/// Diese Filter wirken auf der `Components::*`-API-Ebene, nicht auf den
61/// Component-Body. Da unser `ComponentEquivalent` bereits die
62/// type-spezifischen Ops (`provide_<n>`, `connect_<n>`, etc.) enthaelt,
63/// gibt es typisch nichts zu filtern. Die Filter-Funktion entfernt
64/// dennoch Konfigurator-Operationen, falls vorhanden, und Generic-
65/// Navigation-Ops, falls der Caller sie eingebunden hat.
66///
67/// # Errors
68/// Siehe [`LightweightFilterError`].
69pub fn filter_to_lightweight(
70    eq: ComponentEquivalent,
71) -> Result<ComponentEquivalent, LightweightFilterError> {
72    let kept_exports: Vec<Export> = eq
73        .equivalent_interface
74        .exports
75        .into_iter()
76        .filter(|e| !is_filtered_export(e))
77        .collect();
78    if kept_exports.is_empty() && !eq.event_consumer_interfaces.is_empty() {
79        // Wenn einzige Ports Configurator-Ops waren und sonst nichts,
80        // wird das nach Filter leer — das ist Spec-konform leer
81        // moeglich, aber ein Hinweis fuer den Caller.
82        return Err(LightweightFilterError::EmptyAfterFilter);
83    }
84    let filtered_iface = InterfaceDef {
85        exports: kept_exports,
86        bases: eq
87            .equivalent_interface
88            .bases
89            .into_iter()
90            .filter(|b| !is_filtered_base(b))
91            .collect(),
92        ..eq.equivalent_interface
93    };
94    Ok(ComponentEquivalent {
95        equivalent_interface: filtered_iface,
96        event_consumer_interfaces: eq.event_consumer_interfaces,
97    })
98}
99
100fn is_filtered_export(e: &Export) -> bool {
101    if let Export::Op(o) = e {
102        let n = &o.name.text;
103        // Spec §13.7 (S. 279) — Configurator.
104        matches!(
105            n.as_str(),
106            "configure" | "set_configuration" | "configuration_complete"
107        )
108    } else {
109        false
110    }
111}
112
113fn is_filtered_base(b: &ScopedName) -> bool {
114    // Wir filtern keine Inheritance — Spec §13 entfernt Member-Ops, nicht
115    // die Inheritance-Beziehung selbst. Stub fuer Erweiterbarkeit.
116    let _ = b;
117    false
118}
119
120#[cfg(test)]
121#[allow(clippy::expect_used)]
122mod tests {
123    use super::*;
124    use crate::transform::transform_component;
125    use zerodds_idl::ast::{
126        ComponentDef, ComponentExport, Identifier, OpDecl, ParamAttribute, ParamDecl,
127        PrimitiveType, ScopedName, TypeSpec,
128    };
129    use zerodds_idl::errors::Span;
130
131    fn span() -> Span {
132        Span::SYNTHETIC
133    }
134
135    fn ident(s: &str) -> Identifier {
136        Identifier::new(s, span())
137    }
138
139    fn sn(parts: &[&str]) -> ScopedName {
140        ScopedName {
141            absolute: false,
142            parts: parts.iter().map(|p| ident(p)).collect(),
143            span: span(),
144        }
145    }
146
147    #[test]
148    fn lightweight_filter_drops_configurator_operations() {
149        // Synthese: bauen Equivalent + injizieren `configure`-Op, dann
150        // filtern.
151        let c = ComponentDef {
152            name: ident("C"),
153            base: None,
154            supports: Vec::new(),
155            body: alloc::vec![ComponentExport::Provides {
156                type_spec: sn(&["I"]),
157                name: ident("foo"),
158                span: span(),
159            }],
160            annotations: Vec::new(),
161            span: span(),
162        };
163        let mut eq = transform_component(&c);
164        // Inject Configurator-Op (Spec §6.10.1.1 S. 45).
165        eq.equivalent_interface.exports.push(Export::Op(OpDecl {
166            name: ident("configure"),
167            oneway: false,
168            return_type: None,
169            params: alloc::vec![ParamDecl {
170                attribute: ParamAttribute::In,
171                type_spec: TypeSpec::Primitive(PrimitiveType::Boolean),
172                name: ident("comp"),
173                annotations: Vec::new(),
174                span: span(),
175            }],
176            raises: Vec::new(),
177            annotations: Vec::new(),
178            span: span(),
179        }));
180        let filtered = filter_to_lightweight(eq).expect("filter ok");
181        let names: Vec<String> = filtered
182            .equivalent_interface
183            .exports
184            .iter()
185            .filter_map(|e| match e {
186                Export::Op(o) => Some(o.name.text.clone()),
187                _ => None,
188            })
189            .collect();
190        assert!(names.contains(&String::from("provide_foo")));
191        assert!(!names.contains(&String::from("configure")));
192    }
193
194    #[test]
195    fn lightweight_filter_keeps_typespecific_ops() {
196        // Spec §13.3 entfernt nur GENERIC navigation; type-specific
197        // (provide_<n>) bleibt.
198        let c = ComponentDef {
199            name: ident("C"),
200            base: None,
201            supports: Vec::new(),
202            body: alloc::vec![
203                ComponentExport::Provides {
204                    type_spec: sn(&["I"]),
205                    name: ident("foo"),
206                    span: span(),
207                },
208                ComponentExport::Uses {
209                    type_spec: sn(&["J"]),
210                    name: ident("bar"),
211                    multiple: false,
212                    span: span(),
213                },
214            ],
215            annotations: Vec::new(),
216            span: span(),
217        };
218        let eq = transform_component(&c);
219        let filtered = filter_to_lightweight(eq).expect("filter ok");
220        let names: Vec<String> = filtered
221            .equivalent_interface
222            .exports
223            .iter()
224            .filter_map(|e| match e {
225                Export::Op(o) => Some(o.name.text.clone()),
226                _ => None,
227            })
228            .collect();
229        for expected in [
230            "provide_foo",
231            "connect_bar",
232            "disconnect_bar",
233            "get_connection_bar",
234        ] {
235            assert!(
236                names.contains(&String::from(expected)),
237                "missing {expected}"
238            );
239        }
240    }
241
242    #[test]
243    fn display_error_describes_empty_after_filter() {
244        let s = alloc::format!("{}", LightweightFilterError::EmptyAfterFilter);
245        assert!(s.contains("lightweight"));
246    }
247}