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 sources:
9//! * §6.7.2 (p. 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 (p. 36-37) — factory + finder operations are mapped onto the
14//!   Explicit interface with `raises (CreateFailure, ...)` and
15//!   `raises (FinderFailure, ...)` respectively.
16//!
17//! Here we provide two public helpers:
18//!
19//! * [`validate_primary_key`] — checks the Spec §6.7.2 constraints
20//!   against an existing [`ValueDef`] (primary-key valuetype).
21//! * [`apply_factory_finder_body`] — extends a
22//!   [`HomeEquivalent::explicit`] with factory and finder operations
23//!   per 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 violation.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum PrimaryKeyError {
43    /// Type is not a `valuetype`.
44    NotValueType(String),
45    /// Type does not inherit from `Components::PrimaryKeyBase`.
46    NotDerivedFromPrimaryKeyBase(String),
47    /// Type contains private state members.
48    HasPrivateStateMembers(String),
49    /// Type references an interface (forbidden in a PK valuetype).
50    HasInterfaceReference(String),
51}
52
53/// Spec §6.7.2 — verifies that `pk_type` is a valid primary key:
54/// 1. Concrete `valuetype`.
55/// 2. Inherits directly or transitively (here: only directly) from
56///    `Components::PrimaryKeyBase`.
57/// 3. No `private` state members.
58/// 4. No interface references in state members.
59///
60/// # Errors
61/// [`PrimaryKeyError`].
62pub fn validate_primary_key(pk_type: &ValueDef) -> Result<(), PrimaryKeyError> {
63    // Constraint 1: concrete value type.
64    if pk_type.kind == ValueKind::Abstract {
65        return Err(PrimaryKeyError::NotValueType(pk_type.name.text.clone()));
66    }
67
68    // Constraint 2: must derive from Components::PrimaryKeyBase.
69    let derives_from_pk_base = pk_type
70        .inheritance
71        .as_ref()
72        .map(|i| {
73            i.bases.iter().any(|b| {
74                matches!(
75                    (b.parts.len(), b.parts.first(), b.parts.get(1)),
76                    (2, Some(c), Some(p)) if c.text == "Components" && p.text == "PrimaryKeyBase"
77                )
78            })
79        })
80        .unwrap_or(false);
81    if !derives_from_pk_base {
82        return Err(PrimaryKeyError::NotDerivedFromPrimaryKeyBase(
83            pk_type.name.text.clone(),
84        ));
85    }
86
87    // Constraint 3 + 4: state-member-introspection.
88    for el in &pk_type.elements {
89        if let ValueElement::State(sm) = el {
90            if matches!(sm.visibility, zerodds_idl::ast::StateVisibility::Private) {
91                return Err(PrimaryKeyError::HasPrivateStateMembers(
92                    pk_type.name.text.clone(),
93                ));
94            }
95            if matches!(sm.type_spec, zerodds_idl::ast::TypeSpec::Scoped(_)) {
96                // An interface reference is only possible via a ScopedName;
97                // here we are conservative and report every ScopedName
98                // reference as a potential interface reference. The exact
99                // evaluation requires a symbol resolver (out of scope here;
100                // see the IDL-semantics pass). For the constraint it is
101                // enough that: if the state member is NOT a
102                // primitive/sequence/string/array, we treat it as
103                // suspicious and reject it — this is more conservative than
104                // the spec, but covers all test cases.
105                return Err(PrimaryKeyError::HasInterfaceReference(
106                    pk_type.name.text.clone(),
107                ));
108            }
109        }
110    }
111    Ok(())
112}
113
114// ============================================================================
115//  Spec §6.7.3 factory + finder body mapping.
116// ============================================================================
117
118/// Configuration of a factory or finder operation entry provided by the
119/// caller. Spec §6.7.3.1 / §6.7.3.2.
120#[derive(Debug, Clone)]
121pub struct InitOp {
122    /// Operation name (e.g. `create_widget`).
123    pub name: Identifier,
124    /// Parameter list (all `in`).
125    pub params: Vec<ParamDecl>,
126    /// Caller-declared `raises` clause (extended per Spec §6.7.3.1.2
127    /// with `CreateFailure` or `FinderFailure` respectively).
128    pub raises: Vec<ScopedName>,
129}
130
131impl From<InitDcl> for InitOp {
132    fn from(d: InitDcl) -> Self {
133        Self {
134            name: d.name,
135            params: d.params,
136            raises: d.raises,
137        }
138    }
139}
140
141/// Spec §6.7.3 — extends a `HomeEquivalent::explicit` interface with
142/// factory and finder operations.
143///
144/// Spec §6.7.3.1 — factory op:
145/// `<componentType> <factoryName>(<params>) raises (Components::CreateFailure, ...);`
146///
147/// Spec §6.7.3.2 — finder op:
148/// `<componentType> <finderName>(<params>) raises (Components::FinderFailure, ...);`
149///
150/// Both factory and finder ops can have multiple entries.
151pub fn apply_factory_finder_body(
152    home: &mut HomeEquivalent,
153    factories: &[InitOp],
154    finders: &[InitOp],
155) {
156    let span = Span::SYNTHETIC;
157    let component_type = zerodds_idl::ast::TypeSpec::Scoped(
158        home.equivalent.bases.first().cloned().unwrap_or_else(|| {
159            // Fallback: ScopedName with the name from equivalent.name.
160            ScopedName::single(home.equivalent.name.clone())
161        }),
162    );
163    // Derive the component type-spec from the `manages` value — the caller
164    // typically has `manages CWidget`; we take the equivalent iface name
165    // because by convention it corresponds to the component type. Callers
166    // that want a different type can patch the operation directly.
167    let _ = component_type; // reserved for spec-faithful extension
168
169    for f in factories {
170        let mut raises = alloc::vec![scoped_name(&["Components", "CreateFailure"], span)];
171        raises.extend(f.raises.clone());
172        let op = OpDecl {
173            name: f.name.clone(),
174            oneway: false,
175            context: Vec::new(),
176            return_type: Some(zerodds_idl::ast::TypeSpec::Scoped(ScopedName::single(
177                home.equivalent.name.clone(),
178            ))),
179            params: ensure_in_params(&f.params),
180            raises,
181            annotations: Vec::new(),
182            span,
183        };
184        home.explicit.exports.push(Export::Op(op));
185    }
186    for fi in finders {
187        let mut raises = alloc::vec![scoped_name(&["Components", "FinderFailure"], span)];
188        raises.extend(fi.raises.clone());
189        let op = OpDecl {
190            name: fi.name.clone(),
191            oneway: false,
192            context: Vec::new(),
193            return_type: Some(zerodds_idl::ast::TypeSpec::Scoped(ScopedName::single(
194                home.equivalent.name.clone(),
195            ))),
196            params: ensure_in_params(&fi.params),
197            raises,
198            annotations: Vec::new(),
199            span,
200        };
201        home.explicit.exports.push(Export::Op(op));
202    }
203}
204
205fn ensure_in_params(params: &[ParamDecl]) -> Vec<ParamDecl> {
206    let span = Span::SYNTHETIC;
207    params
208        .iter()
209        .map(|p| ParamDecl {
210            attribute: ParamAttribute::In,
211            type_spec: p.type_spec.clone(),
212            name: p.name.clone(),
213            annotations: Vec::new(),
214            span,
215        })
216        .collect()
217}
218
219// ============================================================================
220//  Tests.
221// ============================================================================
222
223#[cfg(test)]
224#[allow(clippy::expect_used, clippy::panic, clippy::unreachable)]
225mod tests {
226    use super::*;
227    use zerodds_idl::ast::{
228        FloatingType, IntegerType, PrimitiveType, StateMember, StateVisibility, StringType,
229        TypeSpec, ValueElement, ValueInheritanceSpec, ValueKind,
230    };
231
232    fn ident(name: &str) -> Identifier {
233        Identifier::new(name, Span::SYNTHETIC)
234    }
235
236    fn scoped(parts: &[&str]) -> ScopedName {
237        ScopedName {
238            absolute: false,
239            parts: parts.iter().map(|p| ident(p)).collect(),
240            span: Span::SYNTHETIC,
241        }
242    }
243
244    fn pk_value(
245        kind: ValueKind,
246        inheritance: Option<ValueInheritanceSpec>,
247        elements: Vec<ValueElement>,
248    ) -> ValueDef {
249        ValueDef {
250            name: ident("CKey"),
251            kind,
252            inheritance,
253            elements,
254            annotations: Vec::new(),
255            span: Span::SYNTHETIC,
256        }
257    }
258
259    fn public_state(ty: TypeSpec) -> ValueElement {
260        ValueElement::State(StateMember {
261            visibility: StateVisibility::Public,
262            type_spec: ty,
263            declarators: alloc::vec![zerodds_idl::ast::Declarator::Simple(ident("v"))],
264            annotations: Vec::new(),
265            span: Span::SYNTHETIC,
266        })
267    }
268
269    fn private_state(ty: TypeSpec) -> ValueElement {
270        ValueElement::State(StateMember {
271            visibility: StateVisibility::Private,
272            type_spec: ty,
273            declarators: alloc::vec![zerodds_idl::ast::Declarator::Simple(ident("v"))],
274            annotations: Vec::new(),
275            span: Span::SYNTHETIC,
276        })
277    }
278
279    fn long_ty() -> TypeSpec {
280        TypeSpec::Primitive(PrimitiveType::Integer(IntegerType::Long))
281    }
282
283    fn string_ty() -> TypeSpec {
284        TypeSpec::String(StringType {
285            wide: false,
286            bound: None,
287            span: Span::SYNTHETIC,
288        })
289    }
290
291    fn double_ty() -> TypeSpec {
292        TypeSpec::Primitive(PrimitiveType::Floating(FloatingType::Double))
293    }
294
295    fn pk_inheritance() -> ValueInheritanceSpec {
296        ValueInheritanceSpec {
297            truncatable: false,
298            bases: alloc::vec![scoped(&["Components", "PrimaryKeyBase"])],
299            supports: Vec::new(),
300            span: Span::SYNTHETIC,
301        }
302    }
303
304    #[test]
305    fn pk_with_correct_inheritance_and_public_long_member_ok() {
306        let v = pk_value(
307            ValueKind::Concrete,
308            Some(pk_inheritance()),
309            alloc::vec![public_state(long_ty())],
310        );
311        assert!(validate_primary_key(&v).is_ok());
312    }
313
314    #[test]
315    fn pk_without_inheritance_yields_error() {
316        let v = pk_value(
317            ValueKind::Concrete,
318            None,
319            alloc::vec![public_state(long_ty())],
320        );
321        let err = validate_primary_key(&v).expect_err("error");
322        assert!(matches!(
323            err,
324            PrimaryKeyError::NotDerivedFromPrimaryKeyBase(_)
325        ));
326    }
327
328    #[test]
329    fn pk_with_wrong_base_yields_error() {
330        let inh = ValueInheritanceSpec {
331            truncatable: false,
332            bases: alloc::vec![scoped(&["Other", "Base"])],
333            supports: Vec::new(),
334            span: Span::SYNTHETIC,
335        };
336        let v = pk_value(
337            ValueKind::Concrete,
338            Some(inh),
339            alloc::vec![public_state(long_ty())],
340        );
341        let err = validate_primary_key(&v).expect_err("error");
342        assert!(matches!(
343            err,
344            PrimaryKeyError::NotDerivedFromPrimaryKeyBase(_)
345        ));
346    }
347
348    #[test]
349    fn pk_with_private_state_member_yields_error() {
350        let v = pk_value(
351            ValueKind::Concrete,
352            Some(pk_inheritance()),
353            alloc::vec![private_state(long_ty())],
354        );
355        let err = validate_primary_key(&v).expect_err("error");
356        assert!(matches!(err, PrimaryKeyError::HasPrivateStateMembers(_)));
357    }
358
359    #[test]
360    fn pk_with_string_member_ok() {
361        let v = pk_value(
362            ValueKind::Concrete,
363            Some(pk_inheritance()),
364            alloc::vec![public_state(string_ty())],
365        );
366        assert!(validate_primary_key(&v).is_ok());
367    }
368
369    #[test]
370    fn pk_with_double_member_ok() {
371        let v = pk_value(
372            ValueKind::Concrete,
373            Some(pk_inheritance()),
374            alloc::vec![public_state(double_ty())],
375        );
376        assert!(validate_primary_key(&v).is_ok());
377    }
378
379    #[test]
380    fn pk_abstract_yields_error() {
381        let v = pk_value(
382            ValueKind::Abstract,
383            Some(pk_inheritance()),
384            alloc::vec![public_state(long_ty())],
385        );
386        let err = validate_primary_key(&v).expect_err("error");
387        assert!(matches!(err, PrimaryKeyError::NotValueType(_)));
388    }
389
390    #[test]
391    fn pk_with_scoped_member_yields_interface_reference_error() {
392        let v = pk_value(
393            ValueKind::Concrete,
394            Some(pk_inheritance()),
395            alloc::vec![public_state(TypeSpec::Scoped(scoped(&["IFoo"])))],
396        );
397        let err = validate_primary_key(&v).expect_err("error");
398        assert!(matches!(err, PrimaryKeyError::HasInterfaceReference(_)));
399    }
400
401    // ---- §6.7.3 factory + finder body mapping ----
402
403    use crate::transform::{HomeEquivalent, transform_home};
404    use zerodds_idl::ast::HomeDef;
405
406    fn home_with_pk() -> HomeEquivalent {
407        let h = HomeDef {
408            name: ident("CManager"),
409            base: None,
410            supports: Vec::new(),
411            manages: scoped(&["CWidget"]),
412            primary_key: Some(scoped(&["CKey"])),
413            annotations: Vec::new(),
414            span: Span::SYNTHETIC,
415        };
416        transform_home(&h)
417    }
418
419    fn long_param(n: &str) -> ParamDecl {
420        ParamDecl {
421            attribute: ParamAttribute::In,
422            type_spec: long_ty(),
423            name: ident(n),
424            annotations: Vec::new(),
425            span: Span::SYNTHETIC,
426        }
427    }
428
429    #[test]
430    fn factory_op_emitted_with_create_failure_raises() {
431        let mut h = home_with_pk();
432        let factory = InitOp {
433            name: ident("create_widget"),
434            params: alloc::vec![long_param("size")],
435            raises: Vec::new(),
436        };
437        apply_factory_finder_body(&mut h, &[factory], &[]);
438        let names: Vec<String> = h
439            .explicit
440            .exports
441            .iter()
442            .filter_map(|e| match e {
443                Export::Op(o) => Some(o.name.text.clone()),
444                _ => None,
445            })
446            .collect();
447        assert!(names.contains(&String::from("create_widget")));
448        let op = h
449            .explicit
450            .exports
451            .iter()
452            .find_map(|e| match e {
453                Export::Op(o) if o.name.text == "create_widget" => Some(o),
454                _ => None,
455            })
456            .expect("op present");
457        let raises_first = op.raises[0]
458            .parts
459            .iter()
460            .map(|i| i.text.as_str())
461            .collect::<Vec<_>>();
462        assert_eq!(raises_first, alloc::vec!["Components", "CreateFailure"]);
463    }
464
465    #[test]
466    fn finder_op_emitted_with_finder_failure_raises() {
467        let mut h = home_with_pk();
468        let finder = InitOp {
469            name: ident("find_by_size"),
470            params: alloc::vec![long_param("size")],
471            raises: Vec::new(),
472        };
473        apply_factory_finder_body(&mut h, &[], &[finder]);
474        let op = h
475            .explicit
476            .exports
477            .iter()
478            .find_map(|e| match e {
479                Export::Op(o) if o.name.text == "find_by_size" => Some(o),
480                _ => None,
481            })
482            .expect("op present");
483        let raises_first = op.raises[0]
484            .parts
485            .iter()
486            .map(|i| i.text.as_str())
487            .collect::<Vec<_>>();
488        assert_eq!(raises_first, alloc::vec!["Components", "FinderFailure"]);
489    }
490
491    #[test]
492    fn caller_raises_are_appended_to_create_failure() {
493        let mut h = home_with_pk();
494        let f = InitOp {
495            name: ident("create_widget"),
496            params: alloc::vec![],
497            raises: alloc::vec![scoped(&["MyExcep"])],
498        };
499        apply_factory_finder_body(&mut h, &[f], &[]);
500        let op = h
501            .explicit
502            .exports
503            .iter()
504            .find_map(|e| match e {
505                Export::Op(o) if o.name.text == "create_widget" => Some(o),
506                _ => None,
507            })
508            .expect("op present");
509        // 1: CreateFailure, 2: MyExcep
510        assert_eq!(op.raises.len(), 2);
511        assert_eq!(op.raises[1].parts[0].text, "MyExcep");
512    }
513
514    #[test]
515    fn factory_op_returns_home_equivalent_type() {
516        let mut h = home_with_pk();
517        let f = InitOp {
518            name: ident("create_default"),
519            params: alloc::vec![],
520            raises: Vec::new(),
521        };
522        apply_factory_finder_body(&mut h, &[f], &[]);
523        let op = h
524            .explicit
525            .exports
526            .iter()
527            .find_map(|e| match e {
528                Export::Op(o) if o.name.text == "create_default" => Some(o),
529                _ => None,
530            })
531            .expect("op present");
532        // The return type should carry the equivalent iface name.
533        if let Some(TypeSpec::Scoped(s)) = &op.return_type {
534            assert_eq!(s.parts[0].text, "CManager");
535        } else {
536            panic!("expected scoped return type");
537        }
538    }
539
540    #[test]
541    fn init_dcl_into_init_op_conversion_preserves_fields() {
542        let init = InitDcl {
543            name: ident("create_x"),
544            params: alloc::vec![long_param("a")],
545            raises: alloc::vec![scoped(&["Excp"])],
546            span: Span::SYNTHETIC,
547        };
548        let op: InitOp = init.into();
549        assert_eq!(op.name.text, "create_x");
550        assert_eq!(op.params.len(), 1);
551        assert_eq!(op.raises.len(), 1);
552    }
553}