Skip to main content

zerodds_types/
type_matcher.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Writer↔Reader Type-Matching (XTypes §7.2.4 + §7.6.3.7).
4//!
5//! Verbindet [`assignability::is_assignable`] mit der QoS-Policy
6//! [`TypeConsistencyEnforcement`]: je nach TCE-Flags werden einzelne
7//! Assignability-Rules abgeschwaecht oder verschaerft.
8//!
9//! Beispiel:
10//!
11//! ```
12//! use zerodds_types::qos::TypeConsistencyEnforcement;
13//! use zerodds_types::resolve::TypeRegistry;
14//! use zerodds_types::type_matcher::TypeMatcher;
15//! use zerodds_types::{PrimitiveKind, TypeIdentifier};
16//!
17//! let reg = TypeRegistry::new();
18//! let tce = TypeConsistencyEnforcement::default();
19//! let m = TypeMatcher::new(&tce);
20//! let writer = TypeIdentifier::Primitive(PrimitiveKind::Int32);
21//! let reader = TypeIdentifier::Primitive(PrimitiveKind::Int32);
22//! assert!(m.match_types(&writer, &reader, &reg).is_match());
23//! ```
24
25use crate::assignability::{AssignabilityConfig, Assignable, is_assignable};
26use crate::qos::{TypeConsistencyEnforcement, TypeConsistencyKind};
27use crate::resolve::TypeRegistry;
28use crate::type_identifier::TypeIdentifier;
29
30/// Ergebnis eines Type-Matches. Identisch in Semantik zu [`Assignable`],
31/// aber ein eigenstaendiger Typ fuer die Matcher-API (so wird der
32/// Call-Site nicht an das interne `Assignable` gekoppelt).
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum TypeMatchResult {
35    /// Writer-Type und Reader-Type sind fuer ein Match kompatibel.
36    Matches,
37    /// Inkompatibel — statische Begruendung.
38    Incompatible {
39        /// Kurze, statische Begruendung.
40        reason: &'static str,
41    },
42}
43
44impl TypeMatchResult {
45    /// `true` wenn kompatibel.
46    #[must_use]
47    pub const fn is_match(&self) -> bool {
48        matches!(self, Self::Matches)
49    }
50
51    fn from_assignable(a: Assignable) -> Self {
52        match a {
53            Assignable::Yes => Self::Matches,
54            Assignable::No(reason) => Self::Incompatible { reason },
55        }
56    }
57}
58
59/// Facade um [`is_assignable`], die eine [`TypeConsistencyEnforcement`]-
60/// Policy in die interne [`AssignabilityConfig`] uebersetzt.
61///
62/// Keine eigene State — hallt die TCE-Werte zum Call-Time durch.
63#[derive(Debug, Clone, Copy)]
64pub struct TypeMatcher<'a> {
65    tce: &'a TypeConsistencyEnforcement,
66}
67
68impl<'a> TypeMatcher<'a> {
69    /// Konstruktor mit einer TCE-Policy.
70    #[must_use]
71    pub const fn new(tce: &'a TypeConsistencyEnforcement) -> Self {
72        Self { tce }
73    }
74
75    /// Prueft Writer↔Reader Type-Compatibility.
76    ///
77    /// `registry` stellt TypeObjects fuer `EquivalenceHash`-Referenzen
78    /// bereit; ein leerer Registry passt fuer primitive/plain Typen.
79    #[must_use]
80    pub fn match_types(
81        &self,
82        writer: &TypeIdentifier,
83        reader: &TypeIdentifier,
84        registry: &TypeRegistry,
85    ) -> TypeMatchResult {
86        let cfg = self.build_config();
87        TypeMatchResult::from_assignable(is_assignable(writer, reader, registry, &cfg))
88    }
89
90    /// Uebersetzt [`TypeConsistencyEnforcement`] nach
91    /// [`AssignabilityConfig`].
92    ///
93    /// Mapping:
94    /// - `kind == AllowTypeCoercion` ∧ ¬`prevent_type_widening`
95    ///   → `allow_type_coercion = true`.
96    /// - `force_type_validation` → `allow_type_coercion = false`
97    ///   (uebertrumpft die vorige Regel, §7.6.3.7.1).
98    /// - `max_depth` bleibt Default (kommt aus Resolver-Config).
99    fn build_config(&self) -> AssignabilityConfig {
100        let coerce = matches!(self.tce.kind, TypeConsistencyKind::AllowTypeCoercion)
101            && !self.tce.prevent_type_widening;
102        AssignabilityConfig {
103            allow_type_coercion: if self.tce.force_type_validation {
104                false
105            } else {
106                coerce
107            },
108            ignore_sequence_bounds: self.tce.ignore_sequence_bounds,
109            ignore_string_bounds: self.tce.ignore_string_bounds,
110            ignore_member_names: self.tce.ignore_member_names,
111            ignore_literal_names: false,
112            max_depth: crate::resolve::DEFAULT_MAX_RESOLVE_DEPTH,
113        }
114    }
115}
116
117#[cfg(test)]
118#[allow(
119    clippy::unwrap_used,
120    clippy::panic,
121    clippy::field_reassign_with_default
122)]
123mod tests {
124    use super::*;
125    use crate::type_identifier::PrimitiveKind;
126
127    fn reg() -> TypeRegistry {
128        TypeRegistry::new()
129    }
130
131    #[test]
132    fn identical_primitive_matches() {
133        let tce = TypeConsistencyEnforcement::default();
134        let m = TypeMatcher::new(&tce);
135        let w = TypeIdentifier::Primitive(PrimitiveKind::Int32);
136        assert_eq!(m.match_types(&w, &w, &reg()), TypeMatchResult::Matches);
137    }
138
139    #[test]
140    fn widening_allowed_by_default_tce() {
141        // TCE-Default: kind=AllowTypeCoercion, prevent_widening=false.
142        let tce = TypeConsistencyEnforcement::default();
143        let m = TypeMatcher::new(&tce);
144        let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
145        let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
146        assert!(m.match_types(&w, &r, &reg()).is_match());
147    }
148
149    #[test]
150    fn widening_blocked_by_prevent_type_widening() {
151        let mut tce = TypeConsistencyEnforcement::default();
152        tce.prevent_type_widening = true;
153        let m = TypeMatcher::new(&tce);
154        let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
155        let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
156        assert!(!m.match_types(&w, &r, &reg()).is_match());
157    }
158
159    #[test]
160    fn force_type_validation_blocks_coercion() {
161        let mut tce = TypeConsistencyEnforcement::default();
162        tce.force_type_validation = true;
163        let m = TypeMatcher::new(&tce);
164        let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
165        let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
166        assert!(!m.match_types(&w, &r, &reg()).is_match());
167    }
168
169    #[test]
170    fn disallow_type_coercion_blocks_widening() {
171        let mut tce = TypeConsistencyEnforcement::default();
172        tce.kind = TypeConsistencyKind::DisallowTypeCoercion;
173        let m = TypeMatcher::new(&tce);
174        let w = TypeIdentifier::Primitive(PrimitiveKind::Int16);
175        let r = TypeIdentifier::Primitive(PrimitiveKind::Int32);
176        assert!(!m.match_types(&w, &r, &reg()).is_match());
177    }
178
179    #[test]
180    fn incompatible_reports_reason() {
181        let tce = TypeConsistencyEnforcement::default();
182        let m = TypeMatcher::new(&tce);
183        let w = TypeIdentifier::Primitive(PrimitiveKind::Int64);
184        let r = TypeIdentifier::Primitive(PrimitiveKind::Int16);
185        match m.match_types(&w, &r, &reg()) {
186            TypeMatchResult::Incompatible { reason } => {
187                assert!(!reason.is_empty(), "reason must be non-empty");
188            }
189            TypeMatchResult::Matches => {
190                panic!("narrowing i64→i16 must not match");
191            }
192        }
193    }
194}