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
//! The **wire-boundary RM class-invariant dispatcher** — the thin entry points
//! that compose the validation tiers at the codec boundary.
//!
//! The tiers themselves are RM model semantics and live upstream in
//! [`openehr_rm::validate`]: the allocation-free fast path
//! ([`openehr_rm::validate::try_fast_validate`]) and the authoritative typed
//! dispatch ([`openehr_rm::validate::typed_dispatch::dispatch_typed`], the
//! `_type` → concrete-RM-type table). What stays HERE is what genuinely needs
//! this crate:
//!
//! - the GENERATED five-crate structural fallthrough
//! ([`structural_check`]) the typed table declines to — it spans
//! `openehr-base`/`-rm`/`-am`/`-term`/`-lang` at once, so it can only be
//! emitted downstream of all of them;
//! - the undeclared-key refusal, which reads the generated declared-key table
//! ([`declared_fields`]) and renders the reader's own
//! [`crate::json::JsonParseError`];
//! - the entry points themselves ([`validate_rm_value`],
//! [`validate_rm_invariants`], [`validate_rm_invariants_as`],
//! [`validate_rm_invariants_relaxed_as`], [`validate_rm_value_typed`]), which
//! fix the ORDER the tiers and orthogonal layers run in.
//!
//! # The invariant source
//!
//! The RM class invariants live upstream (`openehr-rm`'s generated cores +
//! `*_impl.rs`, per the released class tables' §Invariants — the generated
//! register is the per-invariant authority); this module only routes a node
//! to the right tier. A node that does not deserialize into its declared concrete RM type
//! surfaces `does not conform to RM type …` (see
//! [`openehr_rm::validate::typed_dispatch::record_type_mismatch`]).
use Value;
use InvariantViolation;
use record_type_mismatch;
use crateJsonParseError;
use crate;
/// Run the **core** (non-terminology) RM class invariants for a single
/// canonical-JSON node, dispatching on its `_type`.
///
/// A node with no (or an unrecognised) `_type` runs no invariants (returns
/// without appending).
///
/// Two tiers (performance: the RM-invariant pass visits every `_type` node of a
/// commit, ~1.5k for a populated composition, so the per-node cost is
/// load-bearing):
///
/// 1. the **fast path** ([`openehr_rm::validate::try_fast_validate`]) verifies
/// structural conformance directly against the live JSON node using the
/// generated static RM model and runs the class invariants through the same
/// cores the typed impls call — no deserialization, no allocation,
/// byte-identical output;
/// 2. anything the fast path cannot vouch for falls back to the authoritative
/// **typed dispatch** below ([`validate_rm_value_typed`]), which
/// deserializes into the concrete RM type (surfacing `does not conform to
/// RM type …` for a structural mismatch) and runs the typed invariants.
///
/// This is the tier the fast-path equivalence battery pins (the fast path must
/// vouch only when byte-identical to the typed oracle). The terminology-backed
/// invariants ([`openehr_rm::validate::terminology`]) are an orthogonal layer
/// added by [`validate_rm_value`], not part of the fast/typed equivalence.
/// [`validate_rm_invariants`] with the node's RM type supplied by the caller.
///
/// This is the entry a tree walker uses for a node whose wire `_type` is
/// legitimately absent (canonical JSON requires `_type` only where the declared
/// attribute type is abstract; see
/// [`openehr_rm::model::declared_concrete_type`]). The `_type`-reading
/// wrapper stays for callers with no declaration context.
/// [`validate_rm_invariants_as`] under the `553|incomplete|` **presence
/// relaxation** — the entry the relaxed commit path uses per node.
///
/// RM common `master06-change_control_package.adoc` §Incomplete Content
/// (NOTE): "In the `incomplete` state, a limited form of invalidity is
/// allowed: mandatory attributes may be absent … All other validity
/// requirements must be satisfied. In other words, in an `incomplete` commit,
/// data may be missing, but it may not be wrong."
///
/// The three branches realize that sentence exactly:
///
/// 1. an **undeclared member** is wrong, never missing — refused first, as on
/// the strict path;
/// 2. a node with **nothing missing** ([`mandatory_data_present`](openehr_rm::validate::incomplete::mandatory_data_present))
/// runs the strict tiers unchanged: the relaxation costs it no strictness;
/// 3. a node that **is** missing mandatory data does not have its TYPED
/// construction driven — that tier's refusal IS the presence rule the state
/// lifts (the generated types make existence and cardinality lower bounds
/// structural). Wrongness is still judged: a positive type contradiction
/// ([`contradicts_rm_type`](openehr_rm::validate::incomplete::contradicts_rm_type))
/// is reported through the authoritative typed tier, and the class
/// invariants run wherever the fast path can still vouch for the node as it
/// stands.
///
/// The orthogonal layers the caller runs beside this one — terminology,
/// `LOCATABLE.Archetyped_valid`, the data-structure shape duties, and the
/// archetype-conformance pass — are NOT relaxed here; only the
/// mandatory-presence and cardinality-lower-bound layers are
/// (`openehr_rm::validate::check_mandatory_containers` /
/// `nonempty_list_violations`, which the relaxed walker skips).
/// The violation for the first undeclared member of `value` under class `ty`,
/// or `None` when every member is declared (or `ty` names no emitted struct).
///
/// Byte-identical to what the typed tier reports for the same node: it builds
/// the reader's own error and runs it through [`record_type_mismatch`].
/// Runs **all** RM class invariants for a single canonical-JSON node.
///
/// The set is the core
/// (fast/typed) invariants ([`validate_rm_invariants`]) plus the
/// terminology-backed invariants
/// ([`openehr_rm::validate::terminology::validate_rm_terminology`], the
/// openEHR-terminology group and code-set membership checks the generated
/// invariant cores cannot express mechanically). This is the unified dispatcher
/// every consumer calls; the composition validator invokes it per node and
/// prefixes the absolute RM path onto each [`InvariantViolation`].
/// The typed dispatch tier of [`validate_rm_value`]: deserialize the node
/// into its concrete RM type and run that type's `Validate` impl.
///
/// Authoritative for every node (the fast path may only *skip* it when its
/// result is provably identical); also the oracle the fast-path equivalence
/// tests compare against.
///
/// The tier is composed here from three parts, in this exact order:
///
/// 1. the **undeclared-key refusal**, ahead of any decode (the private
/// `undeclared_key` walk over [`declared_fields`]);
/// 2. the **typed table**
/// ([`openehr_rm::validate::typed_dispatch::dispatch_typed`]), which covers
/// the concrete `openehr-rm` / `openehr-base` types carrying a
/// non-terminology class invariant — those need a typed value to run the
/// invariant on;
/// 3. for every class the table declines (`false`), the **GENERATED structural
/// dispatch** ([`structural_check`], emitted by
/// `openehr-codegen -- emit-json`), which decodes the
/// node into that class's own Rust type and discards it: the codec is the
/// structural-conformance authority for the whole emitted model, so a class
/// with no invariant is still refused when it is structurally defective (a
/// missing mandatory attribute, a wrong JSON kind, an unresolvable nested
/// slot `_type`). This step is what keeps the composition here rather than
/// upstream — the generated dispatch spans every spec crate at once.
///
/// # The inherited LOCATABLE invariant
///
/// After the dispatch runs, the inherited `LOCATABLE.Archetype_node_id_valid`
/// (`not archetype_node_id.is_empty`,
/// `docs/specs/openehr/RM/docs/UML/classes/org.openehr.rm.common.locatable.adoc`
/// §Invariants) is closed out for **every** concrete LOCATABLE descendant via
/// [`openehr_rm::validate::locatable_node_id_violation`], whose applicable-type
/// set is the generated RM model's transitive concrete-descendant closure of
/// LOCATABLE — not a hand-maintained list. The typed table's arms realize it
/// themselves through their typed `Validate` impls, so the violation is appended
/// only when this node's own pass did not already report it (reported exactly
/// once per node). The classes with no arm — `ITEM_TREE`, `ITEM_LIST`,
/// `ITEM_SINGLE`, `EHR_STATUS`, and the demographic / EHR_EXTRACT LOCATABLEs —
/// reach the generated structural fallthrough, which decodes but runs no
/// invariant, so this is where the inherited invariant becomes theirs too.
///
/// Placing the closeout in the typed tier (rather than above both tiers) keeps
/// the fast-vs-typed equivalence property exact: the fast path vouches only for
/// classes whose evaluator already calls the same core, so both tiers report
/// the same single violation.
/// The generated-dispatch fallthrough of [`validate_rm_value_typed`]: decode the
/// node as the class its `_type` names and record a structural violation when it
/// does not conform.
///
/// The classes the typed table
/// ([`openehr_rm::validate::typed_dispatch::dispatch_typed`]) handles already
/// decode there — its arms report the same `does not conform to RM type …` on
/// failure — and the table returns `true` for them, so they never reach here: no
/// node is decoded twice. A `_type` naming no emitted class
/// ([`structural_check`] returns `None`) runs no check, unchanged: an
/// unrecognised type is not a structural claim this layer can adjudicate.