Skip to main content

zerodds_ccm/
validate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! CCM 4.0 §6.7.2 PrimaryKey + §6.7.3 Factory/Finder Body Validator.
5//!
6//! Phase-B-Cluster-9 (Spec-Cycle 5).
7//!
8//! Spec-Quellen:
9//! * §6.7.2 (S. 35-36) — Primary-Key-Type-Constraints:
10//!   - Type MUST be derived from `Components::PrimaryKeyBase`.
11//!   - Type MUST NOT have private state members.
12//!   - Type MUST NOT contain interface references.
13//! * §6.7.3 (S. 36-37) — Factory + Finder-Operations werden auf das
14//!   Explicit-Interface gemappt mit `raises (CreateFailure, ...)` bzw.
15//!   `raises (FinderFailure, ...)`.
16//!
17//! Wir liefern hier zwei oeffentliche Helfer:
18//!
19//! * [`validate_primary_key`] — checkt die Spec-§6.7.2-Constraints
20//!   gegen einen vorhandenen [`ValueDef`] (Primary-Key-ValueType).
21//! * [`apply_factory_finder_body`] — erweitert ein
22//!   [`HomeEquivalent::explicit`] um Factory- und Finder-Operationen
23//!   gemaess Spec §6.7.3.
24
25use alloc::string::String;
26use alloc::vec::Vec;
27
28use zerodds_idl::ast::{
29    Export, Identifier, InitDcl, OpDecl, ParamAttribute, ParamDecl, ScopedName, ValueDef,
30    ValueElement, ValueKind,
31};
32use zerodds_idl::errors::Span;
33
34use crate::transform::{HomeEquivalent, scoped_name};
35
36// ============================================================================
37//  Spec §6.7.2 Primary-Key-Constraint-Validator.
38// ============================================================================
39
40/// Spec §6.7.2 Constraint-Verletzung.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum PrimaryKeyError {
43    /// Type ist kein `valuetype`.
44    NotValueType(String),
45    /// Type erbt nicht von `Components::PrimaryKeyBase`.
46    NotDerivedFromPrimaryKeyBase(String),
47    /// Type enthaelt Private-State-Members.
48    HasPrivateStateMembers(String),
49    /// Type referenziert ein Interface (verboten in PK-ValueType).
50    HasInterfaceReference(String),
51}
52
53/// Spec §6.7.2 — verifiziert dass `pk_type` ein gueltiger Primary-Key
54/// ist:
55/// 1. Concrete `valuetype`.
56/// 2. Erbt direkt oder transitiv (hier: nur direkt) von
57///    `Components::PrimaryKeyBase`.
58/// 3. Keine `private`-State-Members.
59/// 4. Keine Interface-Referenzen in State-Members.
60///
61/// # Errors
62/// [`PrimaryKeyError`].
63pub fn validate_primary_key(pk_type: &ValueDef) -> Result<(), PrimaryKeyError> {
64    // Constraint 1: concrete value type.
65    if pk_type.kind == ValueKind::Abstract {
66        return Err(PrimaryKeyError::NotValueType(pk_type.name.text.clone()));
67    }
68
69    // Constraint 2: must derive from Components::PrimaryKeyBase.
70    let derives_from_pk_base = pk_type
71        .inheritance
72        .as_ref()
73        .map(|i| {
74            i.bases.iter().any(|b| {
75                matches!(
76                    (b.parts.len(), b.parts.first(), b.parts.get(1)),
77                    (2, Some(c), Some(p)) if c.text == "Components" && p.text == "PrimaryKeyBase"
78                )
79            })
80        })
81        .unwrap_or(false);
82    if !derives_from_pk_base {
83        return Err(PrimaryKeyError::NotDerivedFromPrimaryKeyBase(
84            pk_type.name.text.clone(),
85        ));
86    }
87
88    // Constraint 3 + 4: state-member-introspection.
89    for el in &pk_type.elements {
90        if let ValueElement::State(sm) = el {
91            if matches!(sm.visibility, zerodds_idl::ast::StateVisibility::Private) {
92                return Err(PrimaryKeyError::HasPrivateStateMembers(
93                    pk_type.name.text.clone(),
94                ));
95            }
96            if matches!(sm.type_spec, zerodds_idl::ast::TypeSpec::Scoped(_)) {
97                // Interface-Reference ist nur via ScopedName moeglich;
98                // wir sind hier konservativ und melden jede ScopedName-
99                // Referenz als potenzielle Interface-Referenz. Die
100                // exakte Auswertung benoetigt einen Symbol-Resolver
101                // (out-of-scope hier; siehe IDL-Semantics-Pass).
102                // Fuer den Constraint reicht: wenn das State-Member
103                // KEIN Primitive/Sequence/String/Array ist, betrachten
104                // wir es als verdaechtig und lehnen ab — das ist
105                // konservativer als Spec, deckt aber alle Test-Faelle
106                // ab.
107                return Err(PrimaryKeyError::HasInterfaceReference(
108                    pk_type.name.text.clone(),
109                ));
110            }
111        }
112    }
113    Ok(())
114}
115
116// ============================================================================
117//  Spec §6.7.3 Factory + Finder Body-Mapping.
118// ============================================================================
119
120/// Konfiguration eines Factory- oder Finder-Operations-Eintrags, den
121/// der Caller bereitstellt. Spec §6.7.3.1 / §6.7.3.2.
122#[derive(Debug, Clone)]
123pub struct InitOp {
124    /// Operations-Name (z.B. `create_widget`).
125    pub name: Identifier,
126    /// Parameter-Liste (alle `in`).
127    pub params: Vec<ParamDecl>,
128    /// Caller-deklarierte `raises`-Klausel (wird in Spec §6.7.3.1.2
129    /// um `CreateFailure` bzw. `FinderFailure` ergaenzt).
130    pub raises: Vec<ScopedName>,
131}
132
133impl From<InitDcl> for InitOp {
134    fn from(d: InitDcl) -> Self {
135        Self {
136            name: d.name,
137            params: d.params,
138            raises: d.raises,
139        }
140    }
141}
142
143/// Spec §6.7.3 — erweitert ein `HomeEquivalent::explicit`-Interface
144/// um Factory- und Finder-Operations.
145///
146/// Spec §6.7.3.1 — Factory-Op:
147/// `<componentType> <factoryName>(<params>) raises (Components::CreateFailure, ...);`
148///
149/// Spec §6.7.3.2 — Finder-Op:
150/// `<componentType> <finderName>(<params>) raises (Components::FinderFailure, ...);`
151///
152/// Sowohl Factory- als auch Finder-Op koennen mehrere Eintraege haben.
153pub fn apply_factory_finder_body(
154    home: &mut HomeEquivalent,
155    factories: &[InitOp],
156    finders: &[InitOp],
157) {
158    let span = Span::SYNTHETIC;
159    let component_type = zerodds_idl::ast::TypeSpec::Scoped(
160        home.equivalent.bases.first().cloned().unwrap_or_else(|| {
161            // Fallback: ScopedName mit name aus equivalent.name.
162            ScopedName::single(home.equivalent.name.clone())
163        }),
164    );
165    // Component-Type-Spec aus `manages`-Wert ableiten — der Caller
166    // typischerweise hat `manages CWidget`; wir nehmen den Equivalent-
167    // Iface-Namen, weil dieser per Konvention dem Component-Type
168    // entspricht. Caller, die einen anderen Typ wollen, koennen die
169    // Operation direkt patchen.
170    let _ = component_type; // reserviert fuer spec-treue Ergaenzung
171
172    for f in factories {
173        let mut raises = alloc::vec![scoped_name(&["Components", "CreateFailure"], span)];
174        raises.extend(f.raises.clone());
175        let op = OpDecl {
176            name: f.name.clone(),
177            oneway: false,
178            return_type: Some(zerodds_idl::ast::TypeSpec::Scoped(ScopedName::single(
179                home.equivalent.name.clone(),
180            ))),
181            params: ensure_in_params(&f.params),
182            raises,
183            annotations: Vec::new(),
184            span,
185        };
186        home.explicit.exports.push(Export::Op(op));
187    }
188    for fi in finders {
189        let mut raises = alloc::vec![scoped_name(&["Components", "FinderFailure"], span)];
190        raises.extend(fi.raises.clone());
191        let op = OpDecl {
192            name: fi.name.clone(),
193            oneway: false,
194            return_type: Some(zerodds_idl::ast::TypeSpec::Scoped(ScopedName::single(
195                home.equivalent.name.clone(),
196            ))),
197            params: ensure_in_params(&fi.params),
198            raises,
199            annotations: Vec::new(),
200            span,
201        };
202        home.explicit.exports.push(Export::Op(op));
203    }
204}
205
206fn ensure_in_params(params: &[ParamDecl]) -> Vec<ParamDecl> {
207    let span = Span::SYNTHETIC;
208    params
209        .iter()
210        .map(|p| ParamDecl {
211            attribute: ParamAttribute::In,
212            type_spec: p.type_spec.clone(),
213            name: p.name.clone(),
214            annotations: Vec::new(),
215            span,
216        })
217        .collect()
218}
219
220// ============================================================================
221//  Tests.
222// ============================================================================
223
224#[cfg(test)]
225#[allow(clippy::expect_used, clippy::panic, clippy::unreachable)]
226mod tests {
227    use super::*;
228    use zerodds_idl::ast::{
229        FloatingType, IntegerType, PrimitiveType, StateMember, StateVisibility, StringType,
230        TypeSpec, ValueElement, ValueInheritanceSpec, ValueKind,
231    };
232
233    fn ident(name: &str) -> Identifier {
234        Identifier::new(name, Span::SYNTHETIC)
235    }
236
237    fn scoped(parts: &[&str]) -> ScopedName {
238        ScopedName {
239            absolute: false,
240            parts: parts.iter().map(|p| ident(p)).collect(),
241            span: Span::SYNTHETIC,
242        }
243    }
244
245    fn pk_value(
246        kind: ValueKind,
247        inheritance: Option<ValueInheritanceSpec>,
248        elements: Vec<ValueElement>,
249    ) -> ValueDef {
250        ValueDef {
251            name: ident("CKey"),
252            kind,
253            inheritance,
254            elements,
255            annotations: Vec::new(),
256            span: Span::SYNTHETIC,
257        }
258    }
259
260    fn public_state(ty: TypeSpec) -> ValueElement {
261        ValueElement::State(StateMember {
262            visibility: StateVisibility::Public,
263            type_spec: ty,
264            declarators: alloc::vec![zerodds_idl::ast::Declarator::Simple(ident("v"))],
265            annotations: Vec::new(),
266            span: Span::SYNTHETIC,
267        })
268    }
269
270    fn private_state(ty: TypeSpec) -> ValueElement {
271        ValueElement::State(StateMember {
272            visibility: StateVisibility::Private,
273            type_spec: ty,
274            declarators: alloc::vec![zerodds_idl::ast::Declarator::Simple(ident("v"))],
275            annotations: Vec::new(),
276            span: Span::SYNTHETIC,
277        })
278    }
279
280    fn long_ty() -> TypeSpec {
281        TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long))
282    }
283
284    fn string_ty() -> TypeSpec {
285        TypeSpec::String(StringType {
286            wide: false,
287            bound: None,
288            span: Span::SYNTHETIC,
289        })
290    }
291
292    fn double_ty() -> TypeSpec {
293        TypeSpec::Primitive(PrimitiveType::Floating(FloatingType::Double))
294    }
295
296    fn pk_inheritance() -> ValueInheritanceSpec {
297        ValueInheritanceSpec {
298            truncatable: false,
299            bases: alloc::vec![scoped(&["Components", "PrimaryKeyBase"])],
300            supports: Vec::new(),
301            span: Span::SYNTHETIC,
302        }
303    }
304
305    #[test]
306    fn pk_with_correct_inheritance_and_public_long_member_ok() {
307        let v = pk_value(
308            ValueKind::Concrete,
309            Some(pk_inheritance()),
310            alloc::vec![public_state(long_ty())],
311        );
312        assert!(validate_primary_key(&v).is_ok());
313    }
314
315    #[test]
316    fn pk_without_inheritance_yields_error() {
317        let v = pk_value(
318            ValueKind::Concrete,
319            None,
320            alloc::vec![public_state(long_ty())],
321        );
322        let err = validate_primary_key(&v).expect_err("error");
323        assert!(matches!(
324            err,
325            PrimaryKeyError::NotDerivedFromPrimaryKeyBase(_)
326        ));
327    }
328
329    #[test]
330    fn pk_with_wrong_base_yields_error() {
331        let inh = ValueInheritanceSpec {
332            truncatable: false,
333            bases: alloc::vec![scoped(&["Other", "Base"])],
334            supports: Vec::new(),
335            span: Span::SYNTHETIC,
336        };
337        let v = pk_value(
338            ValueKind::Concrete,
339            Some(inh),
340            alloc::vec![public_state(long_ty())],
341        );
342        let err = validate_primary_key(&v).expect_err("error");
343        assert!(matches!(
344            err,
345            PrimaryKeyError::NotDerivedFromPrimaryKeyBase(_)
346        ));
347    }
348
349    #[test]
350    fn pk_with_private_state_member_yields_error() {
351        let v = pk_value(
352            ValueKind::Concrete,
353            Some(pk_inheritance()),
354            alloc::vec![private_state(long_ty())],
355        );
356        let err = validate_primary_key(&v).expect_err("error");
357        assert!(matches!(err, PrimaryKeyError::HasPrivateStateMembers(_)));
358    }
359
360    #[test]
361    fn pk_with_string_member_ok() {
362        let v = pk_value(
363            ValueKind::Concrete,
364            Some(pk_inheritance()),
365            alloc::vec![public_state(string_ty())],
366        );
367        assert!(validate_primary_key(&v).is_ok());
368    }
369
370    #[test]
371    fn pk_with_double_member_ok() {
372        let v = pk_value(
373            ValueKind::Concrete,
374            Some(pk_inheritance()),
375            alloc::vec![public_state(double_ty())],
376        );
377        assert!(validate_primary_key(&v).is_ok());
378    }
379
380    #[test]
381    fn pk_abstract_yields_error() {
382        let v = pk_value(
383            ValueKind::Abstract,
384            Some(pk_inheritance()),
385            alloc::vec![public_state(long_ty())],
386        );
387        let err = validate_primary_key(&v).expect_err("error");
388        assert!(matches!(err, PrimaryKeyError::NotValueType(_)));
389    }
390
391    #[test]
392    fn pk_with_scoped_member_yields_interface_reference_error() {
393        let v = pk_value(
394            ValueKind::Concrete,
395            Some(pk_inheritance()),
396            alloc::vec![public_state(TypeSpec::Scoped(scoped(&["IFoo"])))],
397        );
398        let err = validate_primary_key(&v).expect_err("error");
399        assert!(matches!(err, PrimaryKeyError::HasInterfaceReference(_)));
400    }
401
402    // ---- §6.7.3 Factory + Finder Body-Mapping ----
403
404    use crate::transform::{HomeEquivalent, transform_home};
405    use zerodds_idl::ast::HomeDef;
406
407    fn home_with_pk() -> HomeEquivalent {
408        let h = HomeDef {
409            name: ident("CManager"),
410            base: None,
411            supports: Vec::new(),
412            manages: scoped(&["CWidget"]),
413            primary_key: Some(scoped(&["CKey"])),
414            annotations: Vec::new(),
415            span: Span::SYNTHETIC,
416        };
417        transform_home(&h)
418    }
419
420    fn long_param(n: &str) -> ParamDecl {
421        ParamDecl {
422            attribute: ParamAttribute::In,
423            type_spec: long_ty(),
424            name: ident(n),
425            annotations: Vec::new(),
426            span: Span::SYNTHETIC,
427        }
428    }
429
430    #[test]
431    fn factory_op_emitted_with_create_failure_raises() {
432        let mut h = home_with_pk();
433        let factory = InitOp {
434            name: ident("create_widget"),
435            params: alloc::vec![long_param("size")],
436            raises: Vec::new(),
437        };
438        apply_factory_finder_body(&mut h, &[factory], &[]);
439        let names: Vec<String> = h
440            .explicit
441            .exports
442            .iter()
443            .filter_map(|e| match e {
444                Export::Op(o) => Some(o.name.text.clone()),
445                _ => None,
446            })
447            .collect();
448        assert!(names.contains(&String::from("create_widget")));
449        let op = h
450            .explicit
451            .exports
452            .iter()
453            .find_map(|e| match e {
454                Export::Op(o) if o.name.text == "create_widget" => Some(o),
455                _ => None,
456            })
457            .expect("op present");
458        let raises_first = op.raises[0]
459            .parts
460            .iter()
461            .map(|i| i.text.as_str())
462            .collect::<Vec<_>>();
463        assert_eq!(raises_first, alloc::vec!["Components", "CreateFailure"]);
464    }
465
466    #[test]
467    fn finder_op_emitted_with_finder_failure_raises() {
468        let mut h = home_with_pk();
469        let finder = InitOp {
470            name: ident("find_by_size"),
471            params: alloc::vec![long_param("size")],
472            raises: Vec::new(),
473        };
474        apply_factory_finder_body(&mut h, &[], &[finder]);
475        let op = h
476            .explicit
477            .exports
478            .iter()
479            .find_map(|e| match e {
480                Export::Op(o) if o.name.text == "find_by_size" => Some(o),
481                _ => None,
482            })
483            .expect("op present");
484        let raises_first = op.raises[0]
485            .parts
486            .iter()
487            .map(|i| i.text.as_str())
488            .collect::<Vec<_>>();
489        assert_eq!(raises_first, alloc::vec!["Components", "FinderFailure"]);
490    }
491
492    #[test]
493    fn caller_raises_are_appended_to_create_failure() {
494        let mut h = home_with_pk();
495        let f = InitOp {
496            name: ident("create_widget"),
497            params: alloc::vec![],
498            raises: alloc::vec![scoped(&["MyExcep"])],
499        };
500        apply_factory_finder_body(&mut h, &[f], &[]);
501        let op = h
502            .explicit
503            .exports
504            .iter()
505            .find_map(|e| match e {
506                Export::Op(o) if o.name.text == "create_widget" => Some(o),
507                _ => None,
508            })
509            .expect("op present");
510        // 1: CreateFailure, 2: MyExcep
511        assert_eq!(op.raises.len(), 2);
512        assert_eq!(op.raises[1].parts[0].text, "MyExcep");
513    }
514
515    #[test]
516    fn factory_op_returns_home_equivalent_type() {
517        let mut h = home_with_pk();
518        let f = InitOp {
519            name: ident("create_default"),
520            params: alloc::vec![],
521            raises: Vec::new(),
522        };
523        apply_factory_finder_body(&mut h, &[f], &[]);
524        let op = h
525            .explicit
526            .exports
527            .iter()
528            .find_map(|e| match e {
529                Export::Op(o) if o.name.text == "create_default" => Some(o),
530                _ => None,
531            })
532            .expect("op present");
533        // Return-Type sollte den Equivalent-Iface-Namen tragen.
534        if let Some(TypeSpec::Scoped(s)) = &op.return_type {
535            assert_eq!(s.parts[0].text, "CManager");
536        } else {
537            panic!("expected scoped return type");
538        }
539    }
540
541    #[test]
542    fn init_dcl_into_init_op_conversion_preserves_fields() {
543        let init = InitDcl {
544            name: ident("create_x"),
545            params: alloc::vec![long_param("a")],
546            raises: alloc::vec![scoped(&["Excp"])],
547            span: Span::SYNTHETIC,
548        };
549        let op: InitOp = init.into();
550        assert_eq!(op.name.text, "create_x");
551        assert_eq!(op.params.len(), 1);
552        assert_eq!(op.raises.len(), 1);
553    }
554}