openehr-lang 0.0.2

openEHR LANG 1.0.0: BMM/P_BMM object model (generated from BMM) plus a hand-written ODIN instance parser (the `odin` module — deliberately off the codegen path).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
//! Hand-written spec functions of `BMM_CLASS` (and its `BMM_GENERIC_CLASS`
//! refinement) — the BMM v2 core class-level computed features.
//!
//! Spec: `LANG/docs/bmm/master05-core.adoc` §Semantics — §Basics (the
//! `BMM_CLASSIFIER` naming trio `type_name` / `type_signature` /
//! `conformance_type_name`), §Inheritance ("The evaluation of inheritance
//! relations defined in a BMM schema results in an acyclic graph such that
//! ancestors and descendants can be visualised for any class"), §Classes and
//! Properties ("The features _properties_ and _flat_properties_ defined on
//! `BMM_CLASS` provide access to these two lists for any class") — plus the
//! class definitions `LANG/docs/UML/classes/org.openehr.lang.bmm.bmm_class.adoc`
//! §Functions and `…/org.openehr.lang.bmm.bmm_generic_class.adoc` §Functions.
//! Where the v2 prose is terse, the v3 counterpart
//! (`…/org.openehr.lang.bmm3.bmm_class.adoc` §Functions) states the same
//! function with sharpened wording and is cited at the site it settles.
//!
//! NOTE: the persisted BMM graph is upward-complete but downward nominal — a
//! class embeds its `ancestors` as full `BMM_CLASS` copies, while descendants
//! are recorded only as names (`immediate_descendants`). The two downward
//! functions ([`BmmClass::all_descendants`], [`BmmClass::supplier_closure`])
//! and the primitive-type filter ([`BmmClass::suppliers_non_primitive`],
//! which the class doc grounds "as defined in input schema") therefore take
//! the owning [`BmmModel`] to resolve those names. The parameterless
//! signatures in the class doc assume a live in-memory model with
//! back-references, which the generated persistence-shaped types deliberately
//! do not carry (`org.openehr.lang.bmm.bmm_class.adoc` §Attributes:
//! `immediate_descendants: List<String>`).

use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::VecDeque;

use crate::bmm::core::bmm_class::BmmClass;
use crate::bmm::core::bmm_enumeration::BmmEnumeration;
use crate::bmm::core::bmm_generic_parameter::BmmGenericParameter;
use crate::bmm::core::bmm_model::BmmModel;
use crate::bmm::core::bmm_package::BmmPackage;
use crate::bmm::core::bmm_property::BmmProperty;
use crate::bmm::core::bmm_type::BmmType;

/// The `BMM_CLASS` attributes every generated variant carries, projected out of
/// the variant's own struct so the computed features are written once.
struct ClassCommon<'a> {
    /// `BMM_CLASS.name`.
    name: &'a str,
    /// `BMM_CLASS.ancestors` — immediate inheritance parents, embedded whole.
    ancestors: Option<&'a BTreeMap<String, BmmClass>>,
    /// `BMM_CLASS.package`.
    package: &'a BmmPackage,
    /// `BMM_CLASS.properties` — the *differential* property set.
    properties: Option<&'a BTreeMap<String, BmmProperty<BmmType>>>,
    /// `BMM_CLASS.immediate_descendants` — names only.
    immediate_descendants: &'a [String],
    /// `BMM_CLASS.is_abstract`.
    is_abstract: bool,
    /// `BMM_CLASS.is_primitive_type`.
    is_primitive_type: bool,
}

/// Projects one generated `BMM_CLASS`-family struct onto [`ClassCommon`].
macro_rules! class_common {
    ($c:expr) => {
        ClassCommon {
            name: $c.name.as_str(),
            ancestors: $c.ancestors.as_ref(),
            package: &$c.package,
            properties: $c.properties.as_ref(),
            immediate_descendants: $c.immediate_descendants.as_deref().unwrap_or_default(),
            is_abstract: $c.is_abstract,
            is_primitive_type: $c.is_primitive_type,
        }
    };
}

impl BmmClass {
    /// The `BMM_CLASS` attributes shared by every variant.
    fn common(&self) -> ClassCommon<'_> {
        match self {
            Self::BmmEnumeration(BmmEnumeration::BmmEnumerationInteger(c)) => class_common!(c),
            Self::BmmEnumeration(BmmEnumeration::BmmEnumerationString(c)) => class_common!(c),
            Self::BmmEnumeration(BmmEnumeration::BmmEnumeration(c)) => class_common!(c),
            Self::BmmGenericClass(c) => class_common!(c),
            Self::BmmClass(c) => class_common!(c),
        }
    }

    /// `BMM_CLASS.name`: the root name of this class, "even if the class is
    /// generic" (class doc §Attributes).
    #[must_use]
    pub fn name(&self) -> &str {
        self.common().name
    }

    /// `BMM_CLASS.ancestors`: the immediate inheritance parents, keyed by class
    /// name (class doc §Attributes).
    #[must_use]
    pub fn ancestors(&self) -> Option<&BTreeMap<String, BmmClass>> {
        self.common().ancestors
    }

    /// `BMM_CLASS.package`: the package this class belongs to (class doc
    /// §Attributes).
    #[must_use]
    pub fn package(&self) -> &BmmPackage {
        self.common().package
    }

    /// `BMM_CLASS.properties`: the *differential* property set, i.e. only the
    /// properties this class introduces with respect to its inheritance parents
    /// (`master05-core.adoc` §Classes and Properties). The effective set is
    /// [`Self::flat_properties`].
    #[must_use]
    pub fn properties(&self) -> Option<&BTreeMap<String, BmmProperty<BmmType>>> {
        self.common().properties
    }

    /// `BMM_CLASS.immediate_descendants`: the names of the immediate
    /// inheritance descendants (class doc §Attributes).
    #[must_use]
    pub fn immediate_descendants(&self) -> &[String] {
        self.common().immediate_descendants
    }

    /// `BMM_CLASS.is_abstract`: true if this class is abstract in its model
    /// (class doc §Attributes).
    #[must_use]
    pub fn is_abstract(&self) -> bool {
        self.common().is_abstract
    }

    /// `BMM_CLASS.is_primitive_type`: true if this class is designated a
    /// primitive type "within the overall type system of the schema" (class doc
    /// §Attributes).
    #[must_use]
    pub fn is_primitive_type(&self) -> bool {
        self.common().is_primitive_type
    }

    /// `BMM_GENERIC_CLASS.generic_parameters`: the formal generic parameter
    /// definitions, keyed by parameter name — `Some` only for a generic class
    /// (`org.openehr.lang.bmm.bmm_generic_class.adoc` §Attributes).
    #[must_use]
    pub fn generic_parameters(&self) -> Option<&BTreeMap<String, BmmGenericParameter>> {
        match self {
            Self::BmmGenericClass(c) => Some(&c.generic_parameters),
            Self::BmmEnumeration(_) | Self::BmmClass(_) => None,
        }
    }

    /// `BMM_CLASS.all_ancestors`: "List of all inheritance parent class names,
    /// recursively" (class doc §Functions).
    ///
    /// Walked over the embedded `ancestors` copies — breadth-first over each
    /// map's (sorted) key order, so the result is deterministic — and deduped:
    /// multiple inheritance ("`DV_INTERVAL<T>` multiply inheriting from
    /// `Interval<T>` and `DATA_VALUE`", `master05-core.adoc` §Inheritance) can
    /// reach the same ancestor by more than one path.
    #[must_use]
    pub fn all_ancestors(&self) -> Vec<String> {
        let mut out = Vec::new();
        let mut seen = BTreeSet::new();
        let mut frontier: Vec<&BmmClass> = vec![self];
        while !frontier.is_empty() {
            let mut next: Vec<&BmmClass> = Vec::new();
            for class in frontier {
                for parent in class.ancestors().into_iter().flat_map(BTreeMap::values) {
                    if seen.insert(parent.name().to_owned()) {
                        out.push(parent.name().to_owned());
                        next.push(parent);
                    }
                }
            }
            frontier = next;
        }
        out
    }

    /// `BMM_CLASS.suppliers`: "List of names of immediate supplier classes,
    /// including concrete generic parameters, concrete descendants of abstract
    /// statically defined types, and inherited suppliers. This list includes
    /// primitive types." (class doc §Functions).
    ///
    /// Composed of the flattened type list of every property type
    /// ([`BmmType::flattened_type_list`]), the conformance constraint of every
    /// formal generic parameter for a generic class
    /// (`org.openehr.lang.bmm.bmm_generic_class.adoc` §Functions: "Add
    /// suppliers from generic parameters"), and the same computed recursively
    /// over the embedded ancestors (the "inherited suppliers" clause). The
    /// result is deduped and sorted.
    ///
    /// NOTE (recorded deviation): an *unconstrained* generic parameter
    /// contributes nothing — `org.openehr.lang.bmm3.bmm_class.adoc` §Functions
    /// states the rule the v2 prose leaves implicit ("Where generics are
    /// unconstrained, no class name is added, since logically it would be `Any`
    /// and this can always be assumed anyway"). The v2 clause "concrete
    /// descendants of abstract statically defined types" needs the DOWNWARD
    /// edges, which the persisted class shape records only as names; that
    /// expansion is [`Self::all_descendants`] over a [`BmmModel`], and is
    /// deliberately not folded in here so this function stays a pure function
    /// of the class.
    #[must_use]
    pub fn suppliers(&self) -> Vec<String> {
        let mut acc = BTreeSet::new();
        self.collect_suppliers(&mut acc);
        acc.into_iter().collect()
    }

    /// Accumulates this class's own suppliers and those of its ancestors.
    fn collect_suppliers(&self, acc: &mut BTreeSet<String>) {
        for property in self.properties().into_iter().flat_map(BTreeMap::values) {
            acc.extend(property.flattened_type_list());
        }
        for parameter in self
            .generic_parameters()
            .into_iter()
            .flat_map(BTreeMap::values)
        {
            if let Some(constraint) = parameter.flattened_conforms_to_type() {
                acc.insert(constraint.to_owned());
            }
        }
        for parent in self.ancestors().into_iter().flat_map(BTreeMap::values) {
            parent.collect_suppliers(acc);
        }
    }

    /// `BMM_CLASS.suppliers_non_primitive`: "Same as `suppliers` minus
    /// primitive types, as defined in input schema" (class doc §Functions) —
    /// hence the [`BmmModel`] argument: "as defined in input schema" means the
    /// `is_primitive_type` flag on the supplier's own class definition, not a
    /// property of the supplying reference.
    #[must_use]
    pub fn suppliers_non_primitive(&self, model: &BmmModel) -> Vec<String> {
        self.suppliers()
            .into_iter()
            .filter(|name| {
                !model
                    .class_definition(name)
                    .is_some_and(BmmClass::is_primitive_type)
            })
            .collect()
    }

    /// `BMM_CLASS.supplier_closure`: "List of names of all classes in full
    /// supplier closure, including concrete generic parameters. This list
    /// includes primitive types." (class doc §Functions).
    ///
    /// The transitive closure of [`Self::suppliers`], each supplier name
    /// resolved through [`BmmModel::class_definition`]. A name with no
    /// definition in `model` (a type supplied by an included schema that is not
    /// part of this model) terminates that branch. Cycle-safe: a class already
    /// in the closure is never expanded twice, which is what makes the function
    /// total on the recursive type graphs the RM is full of.
    #[must_use]
    pub fn supplier_closure(&self, model: &BmmModel) -> Vec<String> {
        let mut seen: BTreeSet<String> = BTreeSet::new();
        let mut queue: VecDeque<String> = self.suppliers().into();
        while let Some(name) = queue.pop_front() {
            if !seen.insert(name.clone()) {
                continue;
            }
            if let Some(class) = model.class_definition(&name) {
                for supplier in class.suppliers() {
                    if !seen.contains(&supplier) {
                        queue.push_back(supplier);
                    }
                }
            }
        }
        seen.into_iter().collect()
    }

    /// `BMM_CLASS.all_descendants`: "Compute all descendants by following
    /// immediate_descendants" (class doc §Functions).
    ///
    /// Takes the [`BmmModel`] because `immediate_descendants` holds names, not
    /// class references (module NOTE). Cycle-safe and sorted; the inheritance
    /// graph is acyclic by construction (`master05-core.adoc` §Inheritance),
    /// but a malformed schema must not hang the walk.
    #[must_use]
    pub fn all_descendants(&self, model: &BmmModel) -> Vec<String> {
        let mut seen: BTreeSet<String> = BTreeSet::new();
        let mut queue: VecDeque<String> = self.immediate_descendants().iter().cloned().collect();
        while let Some(name) = queue.pop_front() {
            if !seen.insert(name.clone()) {
                continue;
            }
            if let Some(class) = model.class_definition(&name) {
                queue.extend(class.immediate_descendants().iter().cloned());
            }
        }
        seen.into_iter().collect()
    }

    /// `BMM_CLASS.base_class` (redefined): "Main design class for this type,
    /// from which properties etc can be extracted" (class doc §Functions) — a
    /// class IS its own base class.
    #[must_use]
    pub fn base_class(&self) -> &Self {
        self
    }

    /// `BMM_CLASS.package_path`: "Fully qualified package name, of form:
    /// 'package.package'" (class doc §Functions) — i.e.
    /// [`BmmPackage::path`] of the owning package.
    #[must_use]
    pub fn package_path(&self) -> &str {
        self.package().path()
    }

    /// `BMM_CLASS.class_path`: "Fully qualified class name, of form:
    /// 'package.package.CLASS' with package path in lower-case and class in
    /// original case" (class doc §Functions).
    #[must_use]
    pub fn class_path(&self) -> String {
        let package = self.package_path().to_lowercase();
        let name = self.name();
        format!("{package}.{name}")
    }

    /// `BMM_CLASS.flat_properties`: "List of all properties due to current and
    /// ancestor classes, keyed by property name" (class doc §Functions) — the
    /// *effective* (flat) set, "the result of evaluating these lists of
    /// properties down the inheritance hierarchy" (`master05-core.adoc`
    /// §Classes and Properties).
    ///
    /// Ancestors are merged first (recursively, so the deepest ancestor lands
    /// first) and this class's own differential properties last, so a
    /// redefinition in a nearer class wins over the same property name in a
    /// farther one.
    #[must_use]
    pub fn flat_properties(&self) -> BTreeMap<&str, &BmmProperty<BmmType>> {
        let mut out = BTreeMap::new();
        self.merge_flat_properties(&mut out);
        out
    }

    /// Merges the ancestor-then-own property sets into `out`.
    fn merge_flat_properties<'a>(&'a self, out: &mut BTreeMap<&'a str, &'a BmmProperty<BmmType>>) {
        for parent in self.ancestors().into_iter().flat_map(BTreeMap::values) {
            parent.merge_flat_properties(out);
        }
        for property in self.properties().into_iter().flat_map(BTreeMap::values) {
            out.insert(property.name(), property);
        }
    }

    /// `BMM_CLASS.type_name` (redefined): "Formal string form of the type as
    /// per UML" (class doc §Functions) — the class name for a simple class,
    /// and the generic form `Name<T,U>` over the FORMAL parameter names for a
    /// generic class (`master05-core.adoc` §Basics: "for generic and container
    /// classes it will be generic name such as `List<T>`, `Interval<T>`";
    /// `org.openehr.lang.bmm3.bmm_class.adoc` §Description: "Use `type_name()`
    /// to obtain the qualified type name").
    ///
    /// NOTE (recorded deviation): the persisted `generic_parameters` is a
    /// keyed map, so the FORMAL declaration order the class doc mandates for
    /// `BMM_GENERIC_TYPE.generic_parameters` ("The order must match the order of
    /// the owning class's formal generic parameter declarations") is not
    /// recoverable here; parameters are rendered in the map's sorted key order.
    /// `BMM_GENERIC_PARAMETER` invariant `Inv_generic_name`
    /// (`name.count = 1 and name.is_upper`) keeps that order stable and,
    /// for the conventional `T`, `U`, `V` naming, identical to declaration
    /// order.
    #[must_use]
    pub fn type_name(&self) -> String {
        self.generic_form(|parameter| parameter.name.clone())
    }

    /// `BMM_GENERIC_CLASS.type_signature` (redefined): "Signature form of the
    /// type, which for generics includes generic parameter constrainer types
    /// e.g. Interval<T:Ordered>"
    /// (`org.openehr.lang.bmm.bmm_generic_class.adoc` §Functions). For a
    /// non-generic class this is the class name, i.e.
    /// [`Self::type_name`] (`org.openehr.lang.bmm3.bmm_type.adoc` §Functions:
    /// `type_signature` "Defaults to the value of `type_name()`").
    #[must_use]
    pub fn type_signature(&self) -> String {
        self.generic_form(BmmGenericParameter::type_signature)
    }

    /// The `Name<p1,p2>` rendering of this class, each formal parameter
    /// rendered by `render`; the bare name when the class is not generic.
    ///
    /// Delimiters per `org.openehr.lang.bmm3.bmm_definitions.adoc` §Constants
    /// (`Generic_left_delimiter` `'<'`, `Generic_separator` `','`,
    /// `Generic_right_delimiter` `'>'`).
    fn generic_form(&self, render: impl Fn(&BmmGenericParameter) -> String) -> String {
        match self.generic_parameters() {
            Some(parameters) if !parameters.is_empty() => {
                let rendered: Vec<String> = parameters.values().map(render).collect();
                let name = self.name();
                let joined = rendered.join(",");
                format!("{name}<{joined}>")
            }
            _ => self.name().to_owned(),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use crate::bmm::core::bmm_class::BmmClass;
    use crate::bmm::core::bmm_class::BmmClassData;
    use crate::bmm::core::bmm_generic_class::BmmGenericClass;
    use crate::bmm::core::bmm_generic_parameter::BmmGenericParameter;
    use crate::bmm::core::bmm_package::BmmPackage;
    use crate::bmm::core::bmm_property::BmmProperty;
    use crate::bmm::core::bmm_property::BmmPropertyData;
    use crate::bmm::core::bmm_simple_type::BmmSimpleType;
    use crate::bmm::core::bmm_type::BmmType;

    /// A package node named `name` with no children.
    fn package(name: &str) -> BmmPackage {
        BmmPackage {
            documentation: None,
            packages: None,
            name: name.to_owned(),
            classes: openehr_base::containers::present(Vec::new()),
        }
    }

    /// A `BMM_SIMPLE_CLASS` with the given name, ancestors and properties.
    fn simple_class(
        name: &str,
        ancestors: &[BmmClass],
        properties: &[(&str, BmmType)],
    ) -> BmmClass {
        BmmClass::BmmClass(BmmClassData {
            documentation: None,
            name: name.to_owned(),
            ancestors: if ancestors.is_empty() {
                None
            } else {
                Some(
                    ancestors
                        .iter()
                        .map(|a| (a.name().to_owned(), a.clone()))
                        .collect(),
                )
            },
            package: package("org.openehr.rm.test"),
            properties: if properties.is_empty() {
                None
            } else {
                Some(
                    properties
                        .iter()
                        .map(|(prop, ty)| ((*prop).to_owned(), property(prop, ty.clone())))
                        .collect(),
                )
            },
            source_schema_id: "openehr_test_1.0.0".to_owned(),
            immediate_descendants: openehr_base::containers::present(Vec::new()),
            is_abstract: false,
            is_primitive_type: false,
            is_override: false,
        })
    }

    /// A unitary `BMM_PROPERTY` of the given name and type.
    fn property(name: &str, ty: BmmType) -> BmmProperty<BmmType> {
        BmmProperty::BmmProperty(BmmPropertyData {
            documentation: None,
            name: name.to_owned(),
            is_mandatory: None,
            is_computed: None,
            r#type: ty,
            is_im_runtime: None,
            is_im_infrastructure: None,
        })
    }

    /// A `BMM_SIMPLE_TYPE` over a freshly minted simple class named `name`.
    fn simple_type(name: &str) -> BmmType {
        BmmType::BmmSimpleType(BmmSimpleType {
            documentation: None,
            base_class: simple_class(name, &[], &[]),
        })
    }

    /// A generic parameter, optionally constrained to a class named
    /// `conforms_to`.
    fn generic_parameter(name: &str, conforms_to: Option<&str>) -> BmmGenericParameter {
        BmmGenericParameter {
            documentation: None,
            name: name.to_owned(),
            conforms_to_type: conforms_to.map(|c| simple_class(c, &[], &[])),
            inheritance_precursor: None,
        }
    }

    /// A `BMM_GENERIC_CLASS` with the given formal parameters.
    fn generic_class(name: &str, parameters: &[BmmGenericParameter]) -> BmmClass {
        BmmClass::BmmGenericClass(BmmGenericClass {
            documentation: None,
            name: name.to_owned(),
            ancestors: None,
            package: package("org.openehr.base.foundation_types"),
            properties: None,
            source_schema_id: "openehr_test_1.0.0".to_owned(),
            immediate_descendants: openehr_base::containers::present(Vec::new()),
            is_abstract: false,
            is_primitive_type: false,
            is_override: false,
            generic_parameters: parameters
                .iter()
                .map(|p| (p.name.clone(), p.clone()))
                .collect(),
        })
    }

    #[test]
    fn accessors_read_every_variant() {
        let class = BmmClass::BmmClass(BmmClassData {
            documentation: None,
            name: "LOCATABLE".to_owned(),
            ancestors: None,
            package: package("org.openehr.rm.common.archetyped"),
            properties: None,
            source_schema_id: "openehr_test_1.0.0".to_owned(),
            immediate_descendants: Some(vec!["ENTRY".to_owned()]),
            is_abstract: true,
            is_primitive_type: false,
            is_override: false,
        });
        assert_eq!(class.name(), "LOCATABLE");
        assert!(class.is_abstract());
        assert!(!class.is_primitive_type());
        assert_eq!(class.immediate_descendants(), ["ENTRY".to_owned()]);
        assert_eq!(class.ancestors(), None);
        assert_eq!(class.properties(), None);
        assert_eq!(class.generic_parameters(), None);
        assert_eq!(class.base_class(), &class);
    }

    #[test]
    fn all_ancestors_is_transitive_and_deduped() {
        // ANY <- DATA_VALUE <- DV_ORDERED, and ANY <- DV_ORDERED directly:
        // the diamond must yield ANY exactly once.
        let any = simple_class("ANY", &[], &[]);
        let data_value = simple_class("DATA_VALUE", std::slice::from_ref(&any), &[]);
        let dv_ordered = simple_class("DV_ORDERED", &[data_value, any], &[]);
        assert_eq!(
            dv_ordered.all_ancestors(),
            ["ANY".to_owned(), "DATA_VALUE".to_owned()]
        );
    }

    #[test]
    fn flat_properties_lets_the_nearer_class_override() {
        let ancestor = simple_class(
            "DV_ORDERED",
            &[],
            &[
                ("magnitude", simple_type("Real")),
                ("units", simple_type("String")),
            ],
        );
        let class = simple_class(
            "DV_QUANTITY",
            &[ancestor],
            &[("magnitude", simple_type("Integer"))],
        );
        let flat = class.flat_properties();
        assert_eq!(
            flat.keys().copied().collect::<Vec<_>>(),
            ["magnitude", "units"]
        );
        // The differential (own) definition of `magnitude` wins over the
        // ancestor's: master05-core.adoc §Classes and Properties.
        let magnitude = flat.get("magnitude").expect("magnitude is flat-visible");
        assert_eq!(magnitude.conformance_type_name(), "Integer");
    }

    #[test]
    fn suppliers_walks_properties_generic_parameters_and_ancestors() {
        let ancestor = simple_class(
            "DATA_VALUE",
            &[],
            &[("encoding", simple_type("CODE_PHRASE"))],
        );
        let class = simple_class(
            "DV_TEXT",
            &[ancestor],
            &[
                ("value", simple_type("String")),
                ("mappings", simple_type("TERM_MAPPING")),
            ],
        );
        assert_eq!(
            class.suppliers(),
            [
                "CODE_PHRASE".to_owned(),
                "String".to_owned(),
                "TERM_MAPPING".to_owned()
            ]
        );

        // A generic class adds its constrained parameters; an unconstrained
        // one contributes nothing (bmm3.bmm_class.adoc §Functions).
        let interval = generic_class(
            "Interval",
            &[
                generic_parameter("T", Some("Ordered")),
                generic_parameter("U", None),
            ],
        );
        assert_eq!(interval.suppliers(), ["Ordered".to_owned()]);
    }

    #[test]
    fn naming_trio_renders_the_generic_forms() {
        let interval = generic_class("Interval", &[generic_parameter("T", Some("Ordered"))]);
        assert_eq!(interval.type_name(), "Interval<T>");
        assert_eq!(interval.type_signature(), "Interval<T:Ordered>");

        let hash = generic_class(
            "Hash",
            &[
                generic_parameter("K", Some("Ordered")),
                generic_parameter("V", None),
            ],
        );
        assert_eq!(hash.type_name(), "Hash<K,V>");
        assert_eq!(hash.type_signature(), "Hash<K:Ordered,V>");

        let simple = simple_class("ELEMENT", &[], &[]);
        assert_eq!(simple.type_name(), "ELEMENT");
        assert_eq!(simple.type_signature(), "ELEMENT");
    }

    #[test]
    fn package_and_class_paths_lower_case_only_the_package() {
        let class = simple_class("DV_QUANTITY", &[], &[]);
        assert_eq!(class.package_path(), "org.openehr.rm.test");
        assert_eq!(class.class_path(), "org.openehr.rm.test.DV_QUANTITY");

        let mixed = BmmClass::BmmClass(BmmClassData {
            documentation: None,
            name: "EHR_STATUS".to_owned(),
            ancestors: None,
            package: package("Org.OpenEHR.RM.EHR"),
            properties: None,
            source_schema_id: "openehr_test_1.0.0".to_owned(),
            immediate_descendants: openehr_base::containers::present(Vec::new()),
            is_abstract: false,
            is_primitive_type: false,
            is_override: false,
        });
        assert_eq!(mixed.class_path(), "org.openehr.rm.ehr.EHR_STATUS");
    }

    #[test]
    fn generic_parameters_are_visible_only_on_a_generic_class() {
        let interval = generic_class("Interval", &[generic_parameter("T", Some("Ordered"))]);
        let parameters = interval
            .generic_parameters()
            .expect("a generic class carries formal parameters");
        assert_eq!(parameters.keys().collect::<Vec<_>>(), ["T"]);
        assert_eq!(
            simple_class("ELEMENT", &[], &[]).generic_parameters(),
            None::<&BTreeMap<String, BmmGenericParameter>>
        );
    }
}