openehr-rails 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +2 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +8 -1
- data/docs/backlog.md +12 -1
- data/docs/reports/referral-upstream-log.md +239 -0
- data/lib/openehr_rails/fhir/type_map.rb +2 -0
- data/lib/openehr_rails/fhir/unsupported_profile_error.rb +14 -3
- data/lib/openehr_rails/opt/field_extractor.rb +10 -2
- data/lib/openehr_rails/rm/rm_object_builder.rb +126 -48
- data/lib/openehr_rails/version.rb +1 -1
- data/spec/openehr_rails/aql/dataset_adapter_spec.rb +16 -16
- data/spec/openehr_rails/aql/executor_execute_demo_queries_spec.rb +101 -0
- data/spec/openehr_rails/aql/executor_spec.rb +44 -4
- data/spec/openehr_rails/fhir/fsh_generator_spec.rb +39 -0
- data/spec/openehr_rails/fhir/profile_generator_spec.rb +39 -0
- data/spec/openehr_rails/opt/field_extractor_binding_spec.rb +21 -1
- data/spec/openehr_rails/rm/rm_object_builder_spec.rb +84 -5
- data/spec/support/synthetic_referral_canonical_hash.rb +80 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a30bd4a561f8193c0603c1e43e1873e1c8835d2e7c418b95c429ac6e2b397e83
|
|
4
|
+
data.tar.gz: 180145cf2e294cc68ae84ddc35b147e07e6fb52f99062d14f8df3cb0e306108e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46b84468c263139208e522562831d103cd43d8d3c865aa274fe7cf6db7ebe486fa67329b4e175564a9fc0f94d0a0a4454d679f3d42735ac4c7214b739fa7ba3a
|
|
7
|
+
data.tar.gz: dc189063ef16d43253287e9fed472bdc7d69b0b0c72fb5f393f772cf9adbe596ebd24b6dbea57514435c3a6afe8cdba048fd4b1c3c2e73e39b4a17315ad03b6d
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
> **Upgrade note.** A stored Composition holding a `SECTION` or an `INSTRUCTION`
|
|
13
|
+
> (with its `ACTIVITY`s) now reads back through `to_rm` and AQL; the ones 0.7.1
|
|
14
|
+
> skipped with `openehr-rails AQL: skipping composition uid=...` are exactly the
|
|
15
|
+
> ones that become visible, so query row counts can grow. Two injected defaults
|
|
16
|
+
> are approximations until a later release persists them as columns: an
|
|
17
|
+
> INSTRUCTION without a stored narrative reads back with its node name as
|
|
18
|
+
> `narrative`, and every ACTIVITY with `action_archetype_id` `'/.*/'`.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `RmObjectBuilder` rebuilds `SECTION`, `INSTRUCTION` and `ACTIVITY` nodes (and
|
|
22
|
+
every CARE_ENTRY's `protocol`), so compositions holding them -- a referral's
|
|
23
|
+
`service_request` with its requester/receiver under `protocol[...]`, entries
|
|
24
|
+
nested in a `SECTION` -- become readable through `to_rm` and queryable by AQL
|
|
25
|
+
instead of being skipped with the 0.7.1 warning (#45). Two values are
|
|
26
|
+
**approximations** because the graph does not carry them yet: an INSTRUCTION
|
|
27
|
+
without a stored narrative gets its node name as `narrative`, and every
|
|
28
|
+
ACTIVITY gets `action_archetype_id` `'/.*/'` (any ACTION archetype). Both will
|
|
29
|
+
be persisted as columns in a later release. `ACTION`, `ITEM_SINGLE` and
|
|
30
|
+
`ITEM_TABLE` are still unsupported (skipped with the warning).
|
|
31
|
+
- `FieldExtractor` fields carry `rm_type_alternatives`: every value type the OPT
|
|
32
|
+
allows for the ELEMENT, in OPT order (e.g. `["DV_TEXT", "DV_CODED_TEXT"]` for
|
|
33
|
+
`problem_diagnosis` at0002). Additive; the chosen `rm_type` is unchanged. Lets a
|
|
34
|
+
host app tell an OR-constrained coded text (free text allowed) from a coded-only
|
|
35
|
+
one instead of rendering an empty select (#52, anlage #33).
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- An entry with no extracted leaves (today every `INSTRUCTION`, see #47) no
|
|
39
|
+
longer gets a profile consisting of the archetype `code` rules and an empty
|
|
40
|
+
`component` slicing header; it is skipped and reported through `#skipped`
|
|
41
|
+
with a message saying the entry has no leaves, whatever its resource (#49).
|
|
42
|
+
|
|
10
43
|
## [0.7.1] - 2026-09-25
|
|
11
44
|
|
|
12
45
|
> **Upgrade note.** AQL now skips a stored composition that holds an RM type
|
data/CLAUDE.md
CHANGED
|
@@ -61,6 +61,13 @@ documentation files and `lib/` was byte-identical - but nothing in the release
|
|
|
61
61
|
path caught it. Measurements in `docs/reports/fsh-generator-log.md` R8; the
|
|
62
62
|
matching `release:check` guard is `skoba/openehr-rails#34`.)
|
|
63
63
|
|
|
64
|
+
**Local dev Ruby tracks the patch release CI builds with.** `.ruby-version`
|
|
65
|
+
moved 4.0.6 -> 4.0.7 (2026-10-12) so a local rebuild at the tag stamps the same
|
|
66
|
+
`rubygems_version` (4.0.20) as `ruby/setup-ruby`'s ruby 4.0 image did for 0.7.1,
|
|
67
|
+
whose `metadata.gz` differed from the local rebuild by that one line
|
|
68
|
+
(`docs/reports/referral-upstream-log.md` R2). The cross-check still judges by
|
|
69
|
+
`data.tar.gz`, not the whole-gem sha256: the two toolchains can drift apart again.
|
|
70
|
+
|
|
64
71
|
**Confirm publication by checksum, not by version number.** RubyGems publishes
|
|
65
72
|
the sha256 it recorded at push time (`/api/v1/gems/openehr-rails.json` `sha`, and
|
|
66
73
|
the `checksum:` field in `https://index.rubygems.org/info/openehr-rails`). The
|
|
@@ -225,7 +232,7 @@ This is `openehr-rails`, a Rails engine gem that turns an openEHR Operational Te
|
|
|
225
232
|
|
|
226
233
|
## Development Notes
|
|
227
234
|
|
|
228
|
-
- Ruby 3.3+ required (raised from 3.0 in 2026-08 to track the `openehr` gem dependency dropping Ruby 3.2 support at EOL); CI matrix covers 3.3/3.4/4.0, local dev pinned via `.ruby-version` (currently 4.0.
|
|
235
|
+
- Ruby 3.3+ required (raised from 3.0 in 2026-08 to track the `openehr` gem dependency dropping Ruby 3.2 support at EOL); CI matrix covers 3.3/3.4/4.0, local dev pinned via `.ruby-version` (currently 4.0.7)
|
|
229
236
|
- Rails 7.0+ and Rails 8.x supported (CI matrix: `gemfiles/rails_{7_2,8_0,8_1}.gemfile`; demo app runs on Rails 8.1)
|
|
230
237
|
- URI compatibility workaround implemented for Ruby 3.4
|
|
231
238
|
- `ostruct` is an explicit development dependency — it left Ruby's default gems as of 4.0, and a couple of specs use `OpenStruct` as a lightweight test double
|
data/docs/backlog.md
CHANGED
|
@@ -81,7 +81,18 @@ that verification is deferred to the next real release (>= 0.5.0), not simulated
|
|
|
81
81
|
against the value recorded for that run" cannot be followed literally; the
|
|
82
82
|
tag-rebuild cross-check is what pins the bytes today. Add a "Record sha256" step
|
|
83
83
|
after `rake build` (`sha256sum pkg/*.gem`), as openehr-ruby's `release.yml` has.
|
|
84
|
-
CI-only change; needs a PR, no Issue.
|
|
84
|
+
CI-only change; needs a PR, no Issue. **In PR #51 as `1410274` (2026-10-12,
|
|
85
|
+
`docs/reports/referral-upstream-log.md` R5)**; first run that prints it will be
|
|
86
|
+
the `v0.8.0` tag's.
|
|
87
|
+
- **The tag-rebuild cross-check compares `data.tar.gz`, not the whole `.gem`**
|
|
88
|
+
(0.7.1, 2026-09-25, `docs/reports/referral-upstream-log.md` R2). A `.gem` embeds
|
|
89
|
+
`rubygems_version` in `metadata.gz`, so a rebuild on a different ruby/rubygems
|
|
90
|
+
(local 4.0.6/4.0.16 vs CI 4.0.7/4.0.20) yields a different whole-gem sha256 with
|
|
91
|
+
byte-identical shipped files. Procedure: `tar xf` both gems, compare
|
|
92
|
+
`sha256sum data.tar.gz` (must match) and `diff` the gunzipped `metadata.gz`
|
|
93
|
+
(only `rubygems_version` may differ). 0.7.0's whole-gem match was a coincidence
|
|
94
|
+
of equal toolchains. Fold this into `CLAUDE.md`'s Release convention together
|
|
95
|
+
with the "Record sha256" step above.
|
|
85
96
|
|
|
86
97
|
**Done (2026-08-23, PR #28)** — kept below for the original rationale; see the CI
|
|
87
98
|
status entry above for verification details.
|
|
@@ -62,3 +62,242 @@ release carrying it is 0.8.0.
|
|
|
62
62
|
|
|
63
63
|
Gate when the human has merged what should ride; version number decided from the
|
|
64
64
|
merged content (0.7.1 if patch-only, 0.8.0 if #45 rides).
|
|
65
|
+
|
|
66
|
+
## R2 -- 0.7.1 gate approved and tagged; artifact sha256 differs from the tag rebuild by the rubygems stamp only (2026-09-25)
|
|
67
|
+
|
|
68
|
+
### Gate (approved by the 統括, 2026-09-25)
|
|
69
|
+
|
|
70
|
+
`master` `df93c71` (= #43 + #50 merged): `lib/` touched by `5fcc0ec` (#38) and
|
|
71
|
+
`dd157b6` (#44), both bug fixes -> **patch**; gemspec / version / dependencies
|
|
72
|
+
unchanged; everything else docs. **0.7.1**, #45 deliberately excluded (minor ->
|
|
73
|
+
0.8.0 later, its own inventory). CHANGELOG plan adopted with one addition asked
|
|
74
|
+
for by the ruling: a one-line Upgrade note that AQL now warns and skips instead
|
|
75
|
+
of failing every query, quoting the log line
|
|
76
|
+
`openehr-rails AQL: skipping composition uid=...` so an operator can find it.
|
|
77
|
+
|
|
78
|
+
### Release
|
|
79
|
+
|
|
80
|
+
- `ceb4c5b` Release: bump version to 0.7.1 (`version.rb`, `[0.7.1] - 2026-09-25`
|
|
81
|
+
with the Upgrade note, fresh empty `[Unreleased]`). Before it: full suite
|
|
82
|
+
**311 examples, 0 failures**, rubocop **no offenses**, `release:check OK`;
|
|
83
|
+
`master` CI run 36128248178 success.
|
|
84
|
+
- Tag `v0.7.1` -> `ceb4c5b`, **annotated** this time (`git tag -a`, the R14
|
|
85
|
+
correction applied).
|
|
86
|
+
- `release.yml` run **36128715938**: 12 jobs success, `release:check OK` at the
|
|
87
|
+
tag. Artifact `pkg/openehr-rails-0.7.1.gem`, 251904 bytes,
|
|
88
|
+
sha256 **`29e89993894fe974a1f7eb1cbd51b1809c67aadd1cd5917f4b01af8142159784`**.
|
|
89
|
+
upload-artifact archive digest (not the gem): `216aff34…7c9a`.
|
|
90
|
+
|
|
91
|
+
### The rebuild cross-check did not match, and why that is fine
|
|
92
|
+
|
|
93
|
+
A `gem build` in a scratch worktree at `v0.7.1` (local ruby 4.0.6, rubygems
|
|
94
|
+
4.0.16) gave sha256 `fd06e784…4f8a`, **not** the artifact's. Taken apart
|
|
95
|
+
(`tar xf`; the .gem is a tar of `metadata.gz`, `data.tar.gz`, `checksums.yaml.gz`):
|
|
96
|
+
|
|
97
|
+
| part | CI artifact | local rebuild |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `data.tar.gz` (the shipped files) | `b54d9259…` | `b54d9259…` **identical** |
|
|
100
|
+
| `metadata.gz` | differs on one line: `rubygems_version: 4.0.20` (CI ruby 4.0.7) | `rubygems_version: 4.0.16` |
|
|
101
|
+
| `checksums.yaml.gz` | differs (it hashes metadata) | -- |
|
|
102
|
+
|
|
103
|
+
So the bytes host apps load are identical; only rubygems' own version stamp
|
|
104
|
+
moved, because `ruby/setup-ruby`'s ruby 4.0 image advanced from 4.0.6 to 4.0.7
|
|
105
|
+
since 0.7.0 (when both sides happened to run rubygems 4.0.16 and the whole-gem
|
|
106
|
+
sha256 matched). **Lesson, recorded in `docs/backlog.md`**: the rebuild
|
|
107
|
+
cross-check must compare `data.tar.gz` (and `metadata.gz` minus
|
|
108
|
+
`rubygems_version`), not the whole `.gem`; a whole-gem match is a coincidence of
|
|
109
|
+
equal toolchains. The "Record sha256" CI step in the backlog stays the real pin
|
|
110
|
+
for the published bytes.
|
|
111
|
+
|
|
112
|
+
### Handed to the human
|
|
113
|
+
|
|
114
|
+
`gem push pkg/openehr-rails-0.7.1.gem` (the downloaded CI artifact), then confirm
|
|
115
|
+
`https://rubygems.org/api/v1/gems/openehr-rails.json` `sha` == `29e89993…9784`
|
|
116
|
+
(compact index `checksum:` likewise), then delete the file from `pkg/`. Result
|
|
117
|
+
-> R3. `master` is past the tag by these docs commits (`release:check` on
|
|
118
|
+
`master` fails by design).
|
|
119
|
+
|
|
120
|
+
### Pending
|
|
121
|
+
|
|
122
|
+
#45: the ruling's "conditions a-c" (pre-freeze implementation approved, 0.8.0)
|
|
123
|
+
have not reached this session as text; implementation starts once they do.
|
|
124
|
+
|
|
125
|
+
## R3 -- 0.7.1 published; confirmed by checksum (2026-09-25)
|
|
126
|
+
|
|
127
|
+
Human `gem push` of the CI artifact (R2). Confirmation against the artifact's
|
|
128
|
+
sha256 `29e89993894fe974a1f7eb1cbd51b1809c67aadd1cd5917f4b01af8142159784`:
|
|
129
|
+
|
|
130
|
+
| source | value |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `/api/v1/gems/openehr-rails.json` | version 0.7.1, `sha` `29e89993…9784` |
|
|
133
|
+
| compact index `info/openehr-rails` | `0.7.1 … checksum:29e89993…9784, created_at:2026-09-25T11:43:12Z` |
|
|
134
|
+
| `/api/v1/versions/openehr-rails/latest.json` | `{"version":"0.7.1"}` |
|
|
135
|
+
| `/api/v1/versions/openehr-rails.json` | **still 0.7.0 first** at check time -- the propagation lag CLAUDE.md warns about, observed for real this time |
|
|
136
|
+
|
|
137
|
+
Published gem == CI artifact. `pkg/` emptied. The judgement rule from R2 (tag
|
|
138
|
+
rebuild: `data.tar.gz` identical, metadata differs by `rubygems_version` only)
|
|
139
|
+
stands as the cross-check for future releases.
|
|
140
|
+
|
|
141
|
+
Next: #45 phase A implementation per the ruling (conditions a-c), PR `Fixes #45`,
|
|
142
|
+
then the 0.8.0 inventory.
|
|
143
|
+
|
|
144
|
+
## R4 -- #45 phase A implemented (PR #51), ride-alongs #52 (PR #53) and #49 (PR #54) (2026-09-25)
|
|
145
|
+
|
|
146
|
+
Ruling: #45 phase A approved for pre-freeze implementation with conditions (a)
|
|
147
|
+
injected defaults stated as approximations, (b) regression pin on the existing
|
|
148
|
+
fixtures and the anlage demo queries, (c) #44's SECTION fixture -> ITEM_TABLE;
|
|
149
|
+
ride-alongs to weigh: anlage upstream 19 (`rm_type_alternatives`) and #49.
|
|
150
|
+
|
|
151
|
+
### PR #51 -- `feat/45-rm-object-builder-section-instruction` (Fixes #45)
|
|
152
|
+
|
|
153
|
+
- (b) enhancement. **Red**: 8 new examples on a synthetic canonical hash
|
|
154
|
+
(`spec/support/synthetic_referral_canonical_hash.rb`: SECTION > EVALUATION;
|
|
155
|
+
INSTRUCTION with narrative, protocol ITEM_TREE, one ACTIVITY with description
|
|
156
|
+
and DV_PARSABLE timing) -> `UnsupportedRmTypeError` / empty AQL rows.
|
|
157
|
+
**Green**: 36/0 in the four touched spec files, full suite **324 examples,
|
|
158
|
+
0 failures**, rubocop clean (after one lint follow-up commit for the spec
|
|
159
|
+
file name).
|
|
160
|
+
- `TYPE_CLASSES` += SECTION, INSTRUCTION, ACTIVITY; `build_node` split into
|
|
161
|
+
one helper per stored type (`entry_attributes`, `section_attributes`,
|
|
162
|
+
`instruction_attributes`, `activity_attributes`, `history_attributes`,
|
|
163
|
+
`event_attributes`, `element_attributes`); every CARE_ENTRY gets `protocol`
|
|
164
|
+
when stored. Constructor probes against openehr 2.4.3 before coding:
|
|
165
|
+
`Section.new(items: nil)` ok, `Activity.new(description:, action_archetype_id:)`
|
|
166
|
+
ok with `timing` nil, `DvParsable.new(value:, formalism:)` ok.
|
|
167
|
+
- AQL measured through the new objects: `i/activities[at0001]/description[at0009]/items[at0121]/value/value`,
|
|
168
|
+
`i/protocol[at0008]/items[at0010]/value/value`, and an EVALUATION nested in a
|
|
169
|
+
SECTION via `CONTAINS EVALUATION ev[...]` -- all return the stored values.
|
|
170
|
+
(`CONTAINS SECTION s[...]` as a hop was not needed and not pinned.)
|
|
171
|
+
- (a): `narrative` <- node name when no row; `action_archetype_id` <- `'/.*/'`
|
|
172
|
+
-- stated in the class comment, at the injection sites, in the specs and in
|
|
173
|
+
CHANGELOG `[Unreleased]` Added, as approximations pending phase B.
|
|
174
|
+
- (b): `spec/openehr_rails/aql/executor_execute_demo_queries_spec.rb` pins the
|
|
175
|
+
four anlage demo queries (bmi_calculation graph + a synthetic
|
|
176
|
+
problem_diagnosis composition with the real at-codes). Measured on the way:
|
|
177
|
+
a naive `DV_DATE_TIME` value committed as `2026-02-01T00:00:00` comes back
|
|
178
|
+
as `2026-01-31T15:00:00Z` under JST -- the pin compares instants, not
|
|
179
|
+
strings (pre-existing behaviour, not touched). `protocol` nil pinned on an
|
|
180
|
+
OBSERVATION without one.
|
|
181
|
+
- (c): the #44 fixtures in `dataset_adapter_spec` / `executor_spec` /
|
|
182
|
+
`rm_object_builder_spec` now use an ITEM_TABLE as an EVALUATION's data.
|
|
183
|
+
- CI runs 36131797849 (`ca1af81`) and 36132042465 (`368da91`, lint follow-up):
|
|
184
|
+
success. Semver minor -> 0.8.0.
|
|
185
|
+
|
|
186
|
+
### PR #53 -- `feat/52-rm-type-alternatives` (Fixes #52, filed for upstream 19)
|
|
187
|
+
|
|
188
|
+
Small, so it rides. (b) enhancement: **red** 9/3 (key absent) -> **green** 9/0,
|
|
189
|
+
full suite 314/0. `FieldExtractor` fields gain `rm_type_alternatives` (the
|
|
190
|
+
value's child constraint types in OPT order), `rm_type` selection untouched.
|
|
191
|
+
**Measured before the spec**: at0002 `[DV_TEXT, DV_CODED_TEXT]`, at0073
|
|
192
|
+
`[DV_CODED_TEXT, DV_TEXT]` -- anlage's issue33-plan says at0073 is coded-only;
|
|
193
|
+
the fixture disagrees, and the spec pins the fixture. CI 36132700126 success.
|
|
194
|
+
|
|
195
|
+
### PR #54 -- `fix/49-zero-leaf-skip` (Fixes #49)
|
|
196
|
+
|
|
197
|
+
Small, same decision point as #38, so it rides. (a) bug: **red** 44/4 (a
|
|
198
|
+
0-leaf OBSERVATION and a 0-leaf INSTRUCTION each got a profile with an empty
|
|
199
|
+
`component` slicing header) -> **green** 44/0, full suite 315/0.
|
|
200
|
+
`TypeMap.assert_supported!` raises first for `fields.empty?`;
|
|
201
|
+
`UnsupportedProfileError` says "the entry has no leaves" (pointing at #47).
|
|
202
|
+
Built in a git worktree beside the main checkout (removed afterwards) so #52
|
|
203
|
+
and #49 could proceed in parallel. CI 36132843280 success.
|
|
204
|
+
|
|
205
|
+
### Merge notes for the human
|
|
206
|
+
|
|
207
|
+
All three PRs are CI-green and `MERGEABLE CLEAN` against `560647a`, but each
|
|
208
|
+
adds to the same `[Unreleased]` section of `CHANGELOG.md`, so after the first
|
|
209
|
+
merge the other two will show as conflicting; this session resolves each by
|
|
210
|
+
taking `master`'s CHANGELOG and re-adding the PR's bullet (as done for #43/#50),
|
|
211
|
+
then re-runs CI. Any order works. Then the 0.8.0 inventory (R5).
|
|
212
|
+
|
|
213
|
+
## R5 -- #53 and #54 merged into #51's branch; `release.yml` "Record sha256" rides along; CI green (2026-10-12)
|
|
214
|
+
|
|
215
|
+
Instruction: fold the two ride-alongs into #51 the way #41 was folded into #39
|
|
216
|
+
/ #42 (branch-into-branch merge, identical commit objects), so the human merges
|
|
217
|
+
one PR; then the 0.8.0 inventory.
|
|
218
|
+
|
|
219
|
+
### Branch `feat/45-rm-object-builder-section-instruction` (PR #51), now
|
|
220
|
+
|
|
221
|
+
| commit | what |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `ae71034` | Merge branch `feat/52-rm-type-alternatives` (#53, `d574358`) |
|
|
224
|
+
| `e180eef` | Merge branch `fix/49-zero-leaf-skip` (#54, `7d4d203`) |
|
|
225
|
+
| `1410274` | CI: `release.yml` gains `Record sha256` (`sha256sum pkg/*.gem \| tee pkg/SHA256SUMS`) after `rake build`, ported back from openehr-ruby's copy; `docs/backlog.md` "Release automation" item. CI-only, no Issue (the backlog's own call); rides here so one merge lands it ahead of the `v0.8.0` tag run, which is the first run that will print it. |
|
|
226
|
+
|
|
227
|
+
- Code files were disjoint; the only conflict in both merges was
|
|
228
|
+
`CHANGELOG.md` `[Unreleased]` (as R4 predicted). Resolution: #45's and #52's
|
|
229
|
+
bullets kept in that order under one `### Added`; #49's bullet under a
|
|
230
|
+
`### Fixed` below. `git diff origin/master...HEAD`: 14 files, the union of
|
|
231
|
+
the three PRs' diffs plus `release.yml`.
|
|
232
|
+
- Local, after the merges (bundle had to be installed first -- this checkout
|
|
233
|
+
had no gems): **331 examples, 0 failures**; rubocop **no offenses**
|
|
234
|
+
(111 files).
|
|
235
|
+
- CI: run **38216664617** (`e180eef`, the two merges) success; run
|
|
236
|
+
**38216762262** (`1410274`, final head) **success, 11/11 jobs**. PR #51
|
|
237
|
+
`MERGEABLE CLEAN` against `master` `0434e06`.
|
|
238
|
+
- PR #51's body now names the ride-alongs with `Fixes #52` / `Fixes #49`, so
|
|
239
|
+
the one merge closes #45, #52 and #49.
|
|
240
|
+
|
|
241
|
+
### Handed to the human
|
|
242
|
+
|
|
243
|
+
Merge #51 with a **merge commit** (not squash): then `d574358` and `7d4d203`
|
|
244
|
+
become reachable from `master` as the same objects #53 / #54 point at, exactly
|
|
245
|
+
the #41 situation. After the merge this session comments on #53 and #54
|
|
246
|
+
("taken in via #51") and closes them if GitHub has not already, then runs the
|
|
247
|
+
0.8.0 inventory (`v0.7.1..master`) as a gate report (R6).
|
|
248
|
+
|
|
249
|
+
Inventory preview, measured on the branch (the gate itself waits for the
|
|
250
|
+
merge): `lib/` touched by `rm_object_builder.rb` (#45, enhancement),
|
|
251
|
+
`opt/field_extractor.rb` (#52, additive output), `fhir/type_map.rb` +
|
|
252
|
+
`fhir/unsupported_profile_error.rb` (#49, bug); gemspec / version /
|
|
253
|
+
dependencies untouched; `release.yml` and the docs commits neutral. Expected
|
|
254
|
+
call: **minor -> 0.8.0**, as #45's plan said.
|
|
255
|
+
|
|
256
|
+
## R6 -- #51 merged (with #53/#54 inside); 0.8.0 inventory and gate (2026-10-12)
|
|
257
|
+
|
|
258
|
+
### Step 0: measured on `master` `1185c07` (the #51 merge commit)
|
|
259
|
+
|
|
260
|
+
- `lib/openehr_rails/rm/rm_object_builder.rb:48-53` `TYPE_CLASSES` has
|
|
261
|
+
`SECTION`, `INSTRUCTION`, `ACTIVITY` (#45);
|
|
262
|
+
`lib/openehr_rails/opt/field_extractor.rb:167` emits `rm_type_alternatives`
|
|
263
|
+
(#52); `lib/openehr_rails/fhir/type_map.rb:29` raises first on
|
|
264
|
+
`fields.empty?` (#49); `.github/workflows/release.yml:29-30` has the
|
|
265
|
+
`Record sha256` step.
|
|
266
|
+
- `CHANGELOG.md` `[Unreleased]`: `### Added` with the #45 and #52 bullets,
|
|
267
|
+
`### Fixed` with the #49 bullet -- the R5 resolution survived the merge.
|
|
268
|
+
- #51 merged 13:37 UTC as a merge commit; GitHub marked #53 (`ae71034`) and
|
|
269
|
+
#54 (`e180eef`) merged by reachability, #45/#52/#49 closed. Both PRs got a
|
|
270
|
+
"taken in via #51" comment.
|
|
271
|
+
- `master` CI run **38256998466** (`1185c07`) success. Local under ruby
|
|
272
|
+
4.0.7: **331 examples, 0 failures**, rubocop no offenses (111 files).
|
|
273
|
+
`release:check` on `master` fails by design (HEAD is past the `v0.7.1` tag).
|
|
274
|
+
- #55 (FHIR profile id collision, anlage ledger 23): open issue, no PR,
|
|
275
|
+
nothing in `v0.7.1..master` references it -- stays out, December.
|
|
276
|
+
|
|
277
|
+
### Step 1: inventory `v0.7.1..master`
|
|
278
|
+
|
|
279
|
+
| commit | touches | class |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `b403226` `560647a` `0434e06` `84244cd` | docs/reports, docs/backlog | neutral |
|
|
282
|
+
| `32a79e7` `29a1444` | `.ruby-version`, CLAUDE.md | neutral (dev env / docs) |
|
|
283
|
+
| `1185c07` = #51: `ca1af81` | `rm/rm_object_builder.rb` + specs (#45, enhancement) | **minor** |
|
|
284
|
+
| -- `368da91` | spec file rename | neutral |
|
|
285
|
+
| -- `ae71034` <- `d574358` | `opt/field_extractor.rb` (#52, additive output) | minor |
|
|
286
|
+
| -- `e180eef` <- `7d4d203` | `fhir/type_map.rb`, `fhir/unsupported_profile_error.rb` (#49, bug) | patch |
|
|
287
|
+
| -- `1410274` | `release.yml` Record sha256 | neutral (CI) |
|
|
288
|
+
|
|
289
|
+
Gemspec, `version.rb`, `Gemfile`, `gemfiles/*` untouched. Highest class is
|
|
290
|
+
minor -> **0.8.0**, matching the instructed number.
|
|
291
|
+
|
|
292
|
+
### Prepared, uncommitted, for the release commit
|
|
293
|
+
|
|
294
|
+
`CHANGELOG.md`: `[Unreleased]` -> `[0.8.0] - 2026-10-12` with an Upgrade note
|
|
295
|
+
(two points: SECTION/INSTRUCTION compositions now read back through `to_rm`
|
|
296
|
+
and AQL -- the ones 0.7.1 skipped with the warning are the ones that become
|
|
297
|
+
visible; `narrative` / `action_archetype_id` injected defaults are
|
|
298
|
+
approximations until phase B persists them), the three bullets unchanged, a
|
|
299
|
+
fresh empty `[Unreleased]` above. `version.rb` 0.7.1 -> 0.8.0 goes in the
|
|
300
|
+
same commit (`ceb4c5b`'s shape). Tag `v0.8.0` annotated; the Release run
|
|
301
|
+
prints the gem's sha256 in `Record sha256`, which is the value the human's
|
|
302
|
+
publish and the post-publish checksum are compared against; the tag-rebuild
|
|
303
|
+
cross-check judges by `data.tar.gz` (R2 rule).
|
|
@@ -99,6 +99,8 @@ module OpenehrRails
|
|
|
99
99
|
# path the resource lacks.
|
|
100
100
|
def assert_supported!(entry)
|
|
101
101
|
resource_type = resource_for_entry(entry[:rm_type])
|
|
102
|
+
# No leaves at all: nothing to profile, whatever the resource (#49).
|
|
103
|
+
raise UnsupportedProfileError.new(entry[:archetype_id], resource_type, 0) if entry[:fields].empty?
|
|
102
104
|
return if resource_type == 'Observation' || element_map_for(entry[:archetype_id])
|
|
103
105
|
|
|
104
106
|
raise UnsupportedProfileError.new(entry[:archetype_id], resource_type, entry[:fields].size)
|
|
@@ -17,12 +17,23 @@ module OpenehrRails
|
|
|
17
17
|
@archetype_id = archetype_id
|
|
18
18
|
@resource_type = resource_type
|
|
19
19
|
@leaf_count = leaf_count
|
|
20
|
-
super(
|
|
21
|
-
|
|
20
|
+
super(leaf_count.zero? ? no_leaves_message : unmapped_message)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# 0 leaves: FieldExtractor found no ELEMENT under the entry (today that
|
|
26
|
+
# includes every INSTRUCTION, #47), so there is nothing to profile.
|
|
27
|
+
def no_leaves_message
|
|
28
|
+
"#{archetype_id} (#{resource_type}): the entry has no leaves, nothing to profile -- skipped " \
|
|
29
|
+
'(skoba/openehr-rails#49; INSTRUCTION leaves: #47)'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unmapped_message
|
|
33
|
+
"#{archetype_id}: #{leaf_count} #{leaf_count == 1 ? 'leaf maps' : 'leaves map'} to " \
|
|
22
34
|
"#{resource_type}, which has neither value[x] nor component, and " \
|
|
23
35
|
'TypeMap::ENTRY_ELEMENT_MAPS has no row for this archetype -- skipped ' \
|
|
24
36
|
'(skoba/openehr-rails#33, #38; mapping rows: #35, #37)'
|
|
25
|
-
)
|
|
26
37
|
end
|
|
27
38
|
end
|
|
28
39
|
end
|
|
@@ -10,6 +10,8 @@ module OpenehrRails
|
|
|
10
10
|
# label: display text from the template terminology (any language)
|
|
11
11
|
# path: RM path from the COMPOSITION content
|
|
12
12
|
# rm_type: openEHR data value type (e.g. 'DV_QUANTITY')
|
|
13
|
+
# rm_type_alternatives: every alternative value type the OPT allows, in OPT order
|
|
14
|
+
# (e.g. ['DV_TEXT', 'DV_CODED_TEXT'] for an OR constraint; rm_type is the chosen one)
|
|
13
15
|
# node_id: at-code of the ELEMENT
|
|
14
16
|
# archetype_id: id of the nearest enclosing archetype root (entry or embedded C_ARCHETYPE_ROOT)
|
|
15
17
|
# column_type: ActiveRecord column type symbol
|
|
@@ -162,6 +164,7 @@ module OpenehrRails
|
|
|
162
164
|
label: label || element.node_id,
|
|
163
165
|
path: "#{path}/value",
|
|
164
166
|
rm_type: rm_type,
|
|
167
|
+
rm_type_alternatives: value_children(element).map(&:rm_type_name),
|
|
165
168
|
node_id: element.node_id,
|
|
166
169
|
archetype_id: archetype_id,
|
|
167
170
|
entry_rm_type: entry[:rm_type],
|
|
@@ -176,10 +179,15 @@ module OpenehrRails
|
|
|
176
179
|
field
|
|
177
180
|
end
|
|
178
181
|
|
|
179
|
-
|
|
182
|
+
# All alternative constraints on the ELEMENT's value, in OPT order.
|
|
183
|
+
def value_children(element)
|
|
180
184
|
attrs = element.attributes || []
|
|
181
185
|
value = attrs.find { |a| a.rm_attribute_name == 'value' }
|
|
182
|
-
|
|
186
|
+
value&.children || []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def value_constraint(element)
|
|
190
|
+
children = value_children(element)
|
|
183
191
|
return children.first if children.size <= 1
|
|
184
192
|
|
|
185
193
|
code_reference_class = OpenEHR::AM::OpenEHRProfile::DataTypes::Text::CCodeReference
|
|
@@ -29,11 +29,28 @@ module OpenehrRails
|
|
|
29
29
|
# carried in OPT/stored data. The gem's own constructor validations
|
|
30
30
|
# (ArgumentError on missing mandatory attrs) become the RM-conformance
|
|
31
31
|
# check.
|
|
32
|
+
#
|
|
33
|
+
# Injected values, all documented approximations (#45 ruling condition
|
|
34
|
+
# (a); the graph does not carry them yet, phase B adds columns):
|
|
35
|
+
# - ENTRY language / encoding / subject: OpenehrRails config defaults
|
|
36
|
+
# - HISTORY.origin, EVENT.time: composition context time or now
|
|
37
|
+
# - INSTRUCTION.narrative: the node's name when no narrative row exists
|
|
38
|
+
# - ACTIVITY.action_archetype_id: '/.*/' (any ACTION archetype)
|
|
39
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
32
40
|
class RmObjectBuilder
|
|
33
|
-
#
|
|
41
|
+
# The RM's "any ACTION archetype" pattern, injected because the graph
|
|
42
|
+
# cannot carry ACTIVITY.action_archetype_id yet (see class comment).
|
|
43
|
+
DEFAULT_ACTION_ARCHETYPE_ID = '/.*/'
|
|
44
|
+
|
|
45
|
+
# Map rm_type to the corresponding OpenEHR::RM class. ACTION,
|
|
46
|
+
# ITEM_SINGLE and ITEM_TABLE are persisted by GraphBuilder but not
|
|
47
|
+
# rebuilt here yet (UnsupportedRmTypeError; #44 skips them in AQL).
|
|
34
48
|
TYPE_CLASSES = {
|
|
49
|
+
'SECTION' => OpenEHR::RM::Composition::Content::Navigation::Section,
|
|
35
50
|
'OBSERVATION' => OpenEHR::RM::Composition::Content::Entry::Observation,
|
|
36
51
|
'EVALUATION' => OpenEHR::RM::Composition::Content::Entry::Evaluation,
|
|
52
|
+
'INSTRUCTION' => OpenEHR::RM::Composition::Content::Entry::Instruction,
|
|
53
|
+
'ACTIVITY' => OpenEHR::RM::Composition::Content::Entry::Activity,
|
|
37
54
|
'ADMIN_ENTRY' => OpenEHR::RM::Composition::Content::Entry::AdminEntry,
|
|
38
55
|
'HISTORY' => OpenEHR::RM::DataStructures::History::History,
|
|
39
56
|
'POINT_EVENT' => OpenEHR::RM::DataStructures::History::PointEvent,
|
|
@@ -90,68 +107,125 @@ module OpenehrRails
|
|
|
90
107
|
TYPE_CLASSES.fetch(node.rm_type) { raise UnsupportedRmTypeError.new(@composition, node) }
|
|
91
108
|
end
|
|
92
109
|
|
|
110
|
+
# One helper per stored node type; each adds the attributes the
|
|
111
|
+
# corresponding RM constructor needs, injecting the documented defaults
|
|
112
|
+
# where the graph carries nothing.
|
|
93
113
|
def build_node(node)
|
|
94
114
|
klass = node_class(node)
|
|
95
115
|
attrs = {
|
|
96
116
|
archetype_node_id: node.archetype_node_id,
|
|
97
117
|
name: dv_text(node.name_value || node.archetype_node_id)
|
|
98
118
|
}
|
|
119
|
+
attrs.merge!(entry_attributes(node)) if node.is_a?(OpenehrRails::Rm::EntryNode)
|
|
120
|
+
attrs.merge!(section_attributes(node)) if node.is_a?(OpenehrRails::Rm::Section)
|
|
121
|
+
attrs.merge!(instruction_attributes(node)) if node.is_a?(OpenehrRails::Rm::Instruction)
|
|
122
|
+
attrs.merge!(activity_attributes(node)) if node.is_a?(OpenehrRails::Rm::Activity)
|
|
123
|
+
attrs.merge!(history_attributes(node)) if node.is_a?(OpenehrRails::Rm::History)
|
|
124
|
+
attrs.merge!(event_attributes(node)) if event?(node)
|
|
125
|
+
attrs[:items] = child_nodes(node) if item_container?(node)
|
|
126
|
+
attrs.merge!(element_attributes(node)) if node.is_a?(OpenehrRails::Rm::Element)
|
|
127
|
+
klass.new(attrs)
|
|
128
|
+
end
|
|
99
129
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
130
|
+
# ENTRY: language / encoding / subject are mandatory on every RM ENTRY
|
|
131
|
+
# and not stored, so the config defaults are injected; `data` for the
|
|
132
|
+
# entries that have one; `protocol` for every CARE_ENTRY that stored one
|
|
133
|
+
# (#45 -- INSTRUCTION keeps its requester/receiver there).
|
|
134
|
+
def entry_attributes(node)
|
|
135
|
+
attrs = {
|
|
136
|
+
language: code_phrase(OpenehrRails.default_language, 'ISO_639-1'),
|
|
137
|
+
encoding: code_phrase(OpenehrRails.default_encoding, 'IANA_character-sets'),
|
|
138
|
+
subject: party_self
|
|
139
|
+
}
|
|
140
|
+
data = child_node(node, 'data')
|
|
141
|
+
attrs[:data] = build_node(data) if data
|
|
142
|
+
protocol = child_node(node, 'protocol')
|
|
143
|
+
attrs[:protocol] = build_node(protocol) if protocol
|
|
144
|
+
attrs
|
|
145
|
+
end
|
|
109
146
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
147
|
+
# SECTION.items must be nil or non-empty for the RM constructor.
|
|
148
|
+
def section_attributes(node)
|
|
149
|
+
items = child_nodes(node)
|
|
150
|
+
items.empty? ? {} : { items: items }
|
|
151
|
+
end
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
153
|
+
# INSTRUCTION.narrative is mandatory; the graph stores it as a
|
|
154
|
+
# `narrative` data-value row. When that row is absent the node's name
|
|
155
|
+
# stands in -- an approximation (#45 ruling condition (a)); phase B
|
|
156
|
+
# persists narrative as a column.
|
|
157
|
+
def instruction_attributes(node)
|
|
158
|
+
narrative = data_value_row(node, 'narrative')
|
|
159
|
+
attrs = { narrative: narrative ? dv_text(narrative.text_value) : dv_text(node.name_value || node.archetype_node_id) }
|
|
160
|
+
activities = child_nodes(node, 'activities')
|
|
161
|
+
attrs[:activities] = activities unless activities.empty?
|
|
162
|
+
expiry = data_value_row(node, 'expiry_time')
|
|
163
|
+
attrs[:expiry_time] = dv_date_time(expiry.datetime_value) if expiry&.datetime_value
|
|
164
|
+
attrs
|
|
165
|
+
end
|
|
123
166
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
167
|
+
# ACTIVITY.description is mandatory and never defaulted: a graph without
|
|
168
|
+
# one fails RM conformance (the gem raises, #44 reports and skips).
|
|
169
|
+
# action_archetype_id is mandatory too but the graph cannot carry it
|
|
170
|
+
# (GraphBuilder drops String-valued attributes), so the RM's "any ACTION
|
|
171
|
+
# archetype" pattern is injected -- an approximation (#45 ruling
|
|
172
|
+
# condition (a)); phase B persists the real value.
|
|
173
|
+
def activity_attributes(node)
|
|
174
|
+
attrs = { action_archetype_id: DEFAULT_ACTION_ARCHETYPE_ID }
|
|
175
|
+
description = child_node(node, 'description')
|
|
176
|
+
attrs[:description] = build_node(description) if description
|
|
177
|
+
timing = data_value_row(node, 'timing')
|
|
178
|
+
attrs[:timing] = dv_parsable(timing.text_value, timing.formalism) if timing
|
|
179
|
+
attrs
|
|
180
|
+
end
|
|
136
181
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
182
|
+
# HISTORY.origin is mandatory.
|
|
183
|
+
def history_attributes(node)
|
|
184
|
+
{
|
|
185
|
+
origin: dv_date_time(node.history_origin || node.composition.context_start_time || Time.current),
|
|
186
|
+
events: child_nodes(node)
|
|
187
|
+
}
|
|
188
|
+
end
|
|
142
189
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
190
|
+
def event?(node)
|
|
191
|
+
node.is_a?(OpenehrRails::Rm::PointEvent) || node.is_a?(OpenehrRails::Rm::IntervalEvent)
|
|
192
|
+
end
|
|
147
193
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
194
|
+
# EVENT.time and .data are mandatory; INTERVAL_EVENT adds width and
|
|
195
|
+
# math_function.
|
|
196
|
+
def event_attributes(node)
|
|
197
|
+
attrs = { time: dv_date_time(node.event_time || Time.current), data: build_node(child_node(node, 'data')) }
|
|
198
|
+
if node.is_a?(OpenehrRails::Rm::IntervalEvent)
|
|
199
|
+
attrs[:width] = parse_duration(node.width || 'PT0S')
|
|
200
|
+
attrs[:math_function] = code_phrase(node.math_function_code || '144', 'openehr')
|
|
152
201
|
end
|
|
202
|
+
attrs
|
|
203
|
+
end
|
|
153
204
|
|
|
154
|
-
|
|
205
|
+
def item_container?(node)
|
|
206
|
+
node.is_a?(OpenehrRails::Rm::ItemTree) || node.is_a?(OpenehrRails::Rm::ItemList) ||
|
|
207
|
+
node.is_a?(OpenehrRails::Rm::Cluster)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def element_attributes(node)
|
|
211
|
+
dv = node.data_values.first
|
|
212
|
+
dv ? { value: build_data_value(dv) } : {}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def child_node(node, attribute)
|
|
216
|
+
node.children.find { |c| c.rm_attribute_name == attribute }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Children in position order, optionally restricted to one RM attribute
|
|
220
|
+
# (INSTRUCTION has `activities` and `protocol` children side by side).
|
|
221
|
+
def child_nodes(node, attribute = nil)
|
|
222
|
+
children = node.children.sort_by(&:position)
|
|
223
|
+
children = children.select { |c| c.rm_attribute_name == attribute } if attribute
|
|
224
|
+
children.map { |c| build_node(c) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def data_value_row(node, attribute)
|
|
228
|
+
node.data_values.find { |dv| dv.rm_attribute_name == attribute }
|
|
155
229
|
end
|
|
156
230
|
|
|
157
231
|
def build_data_value(dv)
|
|
@@ -202,6 +276,10 @@ module OpenehrRails
|
|
|
202
276
|
OpenEHR::RM::DataTypes::Text::DvText.new(value: value.to_s)
|
|
203
277
|
end
|
|
204
278
|
|
|
279
|
+
def dv_parsable(value, formalism)
|
|
280
|
+
OpenEHR::RM::DataTypes::Encapsulated::DvParsable.new(value: value.to_s, formalism: formalism)
|
|
281
|
+
end
|
|
282
|
+
|
|
205
283
|
def dv_coded_text(value, code, terminology)
|
|
206
284
|
OpenEHR::RM::DataTypes::Text::DvCodedText.new(
|
|
207
285
|
value: value,
|
|
@@ -57,28 +57,28 @@ describe OpenehrRails::Aql::DatasetAdapter do
|
|
|
57
57
|
|
|
58
58
|
# skoba/openehr-rails#44, resolution shape (a) bug: one composition whose
|
|
59
59
|
# graph cannot be rebuilt as RM objects used to fail every query in the
|
|
60
|
-
# store. Synthetic canonical hash (invented ids):
|
|
61
|
-
# GraphBuilder (Rm::TypeMap::NODE_TYPES)
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# unsupported type (ITEM_TABLE is the next candidate).
|
|
60
|
+
# store. Synthetic canonical hash (invented ids): an ITEM_TABLE as an
|
|
61
|
+
# EVALUATION's data is accepted by GraphBuilder (Rm::TypeMap::NODE_TYPES)
|
|
62
|
+
# but not by RmObjectBuilder::TYPE_CLASSES. (It was a SECTION until #45
|
|
63
|
+
# made SECTION buildable -- ruling condition (c).)
|
|
65
64
|
describe 'a composition whose to_rm fails' do
|
|
66
|
-
let(:
|
|
65
|
+
let(:table_hash) do
|
|
67
66
|
{
|
|
68
67
|
'_type' => 'COMPOSITION',
|
|
69
|
-
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.
|
|
68
|
+
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1',
|
|
70
69
|
'archetype_details' => {
|
|
71
70
|
'_type' => 'ARCHETYPED',
|
|
72
|
-
'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.
|
|
73
|
-
'template_id' => { 'value' => '
|
|
71
|
+
'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1' },
|
|
72
|
+
'template_id' => { 'value' => 'synthetic_table_test' },
|
|
74
73
|
'rm_version' => '1.0.4'
|
|
75
74
|
},
|
|
76
75
|
'content' => [
|
|
77
76
|
{
|
|
78
|
-
'_type' => '
|
|
79
|
-
'archetype_node_id' => 'openEHR-EHR-
|
|
80
|
-
'
|
|
81
|
-
'
|
|
77
|
+
'_type' => 'EVALUATION',
|
|
78
|
+
'archetype_node_id' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1',
|
|
79
|
+
'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1' } },
|
|
80
|
+
'name' => { '_type' => 'DV_TEXT', 'value' => 'Synthetic table' },
|
|
81
|
+
'data' => { '_type' => 'ITEM_TABLE', 'archetype_node_id' => 'at0001', 'rows' => [] }
|
|
82
82
|
}
|
|
83
83
|
]
|
|
84
84
|
}
|
|
@@ -87,18 +87,18 @@ describe OpenehrRails::Aql::DatasetAdapter do
|
|
|
87
87
|
before do
|
|
88
88
|
BmiCalculation.create!(height: 170.0, ehr_id: 'ehr-mixed')
|
|
89
89
|
ehr = OpenehrRails::Rm::Ehr.find_by!(ehr_id: 'ehr-mixed')
|
|
90
|
-
OpenehrRails::Rm::CompositionCommitter.commit(
|
|
90
|
+
OpenehrRails::Rm::CompositionCommitter.commit(table_hash, uid: 'uid-table', ehr: ehr)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
it 'warns, skips that composition, and keeps the rest of the EHR queryable' do
|
|
94
94
|
records = nil
|
|
95
95
|
|
|
96
96
|
expect { records = described_class.build.each_ehr.to_a }
|
|
97
|
-
.to output(/uid-
|
|
97
|
+
.to output(/uid-table.*UnsupportedRmTypeError.*ITEM_TABLE/).to_stderr
|
|
98
98
|
|
|
99
99
|
matching = records.find { |r| r.ehr_id == 'ehr-mixed' }
|
|
100
100
|
expect(matching.compositions.size).to eq(1)
|
|
101
|
-
expect(matching.compositions.map { |c| c.uid.value }).not_to include('uid-
|
|
101
|
+
expect(matching.compositions.map { |c| c.uid.value }).not_to include('uid-table')
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'openehr'
|
|
5
|
+
require 'openehr/aql'
|
|
6
|
+
require 'openehr_rails'
|
|
7
|
+
require_relative '../storable_spec_model'
|
|
8
|
+
|
|
9
|
+
# Regression pin (skoba/openehr-rails#45, ruling condition (b)): the four AQL
|
|
10
|
+
# queries the anlage demo runs (anlage docs/demo/aql-queries.md 1-4) must keep
|
|
11
|
+
# returning the same rows after RmObjectBuilder started rebuilding `protocol`,
|
|
12
|
+
# SECTION, INSTRUCTION and ACTIVITY. Resolution shape (c): these results were
|
|
13
|
+
# already true before #45 and red is not achievable -- this file pins them.
|
|
14
|
+
#
|
|
15
|
+
# Data: query 1 and 3 run on the BmiCalculation model (real bmi_calculation.opt
|
|
16
|
+
# graph). Queries 2 and 4 need a problem_diagnosis EVALUATION, for which this
|
|
17
|
+
# repo has no model, so a **synthetic** canonical hash is committed directly:
|
|
18
|
+
# hand-authored, but its archetype id and at-codes are the real ones from
|
|
19
|
+
# spec/templates/problem_list.opt (at0002 diagnosis, at0073 certainty with the
|
|
20
|
+
# local code at0076, at0003 date/time recognised) so the demo queries run
|
|
21
|
+
# verbatim. Design authority: docs/design/rm-object-builder-section-instruction-plan.md
|
|
22
|
+
# section 3.
|
|
23
|
+
describe OpenehrRails::Aql::Executor, '.execute' do
|
|
24
|
+
# anlage demo AQL queries (regression pin) -- see the file comment above.
|
|
25
|
+
let(:problem_diagnosis_hash) do
|
|
26
|
+
{
|
|
27
|
+
'_type' => 'COMPOSITION',
|
|
28
|
+
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.problem_list.v1',
|
|
29
|
+
'archetype_details' => {
|
|
30
|
+
'_type' => 'ARCHETYPED',
|
|
31
|
+
'archetype_id' => { 'value' => 'openEHR-EHR-COMPOSITION.problem_list.v1' },
|
|
32
|
+
'template_id' => { 'value' => 'problem_list' }, 'rm_version' => '1.0.4'
|
|
33
|
+
},
|
|
34
|
+
'name' => { '_type' => 'DV_TEXT', 'value' => 'Problem list' },
|
|
35
|
+
'content' => [
|
|
36
|
+
{
|
|
37
|
+
'_type' => 'EVALUATION', 'archetype_node_id' => 'openEHR-EHR-EVALUATION.problem_diagnosis.v1',
|
|
38
|
+
'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.problem_diagnosis.v1' } },
|
|
39
|
+
'name' => { '_type' => 'DV_TEXT', 'value' => 'Problem/Diagnosis' },
|
|
40
|
+
'data' => {
|
|
41
|
+
'_type' => 'ITEM_TREE', 'archetype_node_id' => 'at0001',
|
|
42
|
+
'items' => [
|
|
43
|
+
{ '_type' => 'ELEMENT', 'archetype_node_id' => 'at0002',
|
|
44
|
+
'value' => { '_type' => 'DV_TEXT', 'value' => 'Hypertension' } },
|
|
45
|
+
{ '_type' => 'ELEMENT', 'archetype_node_id' => 'at0073',
|
|
46
|
+
'value' => { '_type' => 'DV_CODED_TEXT', 'value' => 'Confirmed',
|
|
47
|
+
'defining_code' => { 'terminology_id' => { 'value' => 'local' }, 'code_string' => 'at0076' } } },
|
|
48
|
+
{ '_type' => 'ELEMENT', 'archetype_node_id' => 'at0003',
|
|
49
|
+
'value' => { '_type' => 'DV_DATE_TIME', 'value' => '2026-02-01T00:00:00' } }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
before do
|
|
58
|
+
BmiCalculation.create!(height: 170.0)
|
|
59
|
+
BmiCalculation.create!(height: 180.0)
|
|
60
|
+
OpenehrRails::Rm::CompositionCommitter.commit(problem_diagnosis_hash, uid: 'uid-demo-problem')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it '1. height inequality (alias in SELECT, path in WHERE)' do
|
|
64
|
+
query = 'SELECT o/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude AS height ' \
|
|
65
|
+
'FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.height.v2] ' \
|
|
66
|
+
'WHERE o/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value/magnitude > 170'
|
|
67
|
+
|
|
68
|
+
expect(OpenehrRails::Aql.execute(query).rows).to eq([[180.0]])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it '2. MATCHES against a literal value list on the certainty label' do
|
|
72
|
+
query = 'SELECT c/name/value AS composition_name, o/data[at0001]/items[at0073]/value/value AS certainty ' \
|
|
73
|
+
'FROM EHR e CONTAINS COMPOSITION c CONTAINS EVALUATION o[openEHR-EHR-EVALUATION.problem_diagnosis.v1] ' \
|
|
74
|
+
"WHERE o/data[at0001]/items[at0073]/value/value MATCHES {'Confirmed', 'Suspected'}"
|
|
75
|
+
|
|
76
|
+
expect(OpenehrRails::Aql.execute(query).rows).to eq([['Problem list', 'Confirmed']])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it '3. CONTAINS nodePredicate with WHERE EXISTS' do
|
|
80
|
+
query = 'SELECT c/name/value AS composition_name ' \
|
|
81
|
+
'FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.height.v2] ' \
|
|
82
|
+
'CONTAINS ELEMENT el[at0004] WHERE EXISTS el/value/magnitude'
|
|
83
|
+
|
|
84
|
+
expect(OpenehrRails::Aql.execute(query).rows)
|
|
85
|
+
.to eq([['openEHR-EHR-COMPOSITION.report-result.v1'], ['openEHR-EHR-COMPOSITION.report-result.v1']])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it '4. date range WHERE on an ELEMENT-held date/time' do
|
|
89
|
+
query = 'SELECT o/data[at0001]/items[at0003]/value/value AS recognized_at ' \
|
|
90
|
+
'FROM EHR e CONTAINS COMPOSITION c CONTAINS EVALUATION o[openEHR-EHR-EVALUATION.problem_diagnosis.v1] ' \
|
|
91
|
+
'WHERE o/data[at0001]/items[at0003]/value/value >= "2026-01-01T00:00:00"'
|
|
92
|
+
|
|
93
|
+
rows = OpenehrRails::Aql.execute(query).rows
|
|
94
|
+
|
|
95
|
+
# Measured: the naive input is parsed in the process's local zone and
|
|
96
|
+
# comes back as UTC (`2026-01-31T15:00:00Z` under JST) -- pinned as the
|
|
97
|
+
# same instant, not as the literal string.
|
|
98
|
+
expect(rows.size).to eq(1)
|
|
99
|
+
expect(Time.iso8601(rows.first.first)).to eq(Time.parse('2026-02-01T00:00:00').utc)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -104,24 +104,64 @@ describe OpenehrRails::Aql::Executor do
|
|
|
104
104
|
describe 'with a composition whose to_rm fails in the store' do
|
|
105
105
|
before do
|
|
106
106
|
BmiCalculation.create!(height: 170.0)
|
|
107
|
+
# ITEM_TABLE stays outside RmObjectBuilder::TYPE_CLASSES after #45
|
|
108
|
+
# made SECTION buildable (ruling condition (c)).
|
|
107
109
|
OpenehrRails::Rm::CompositionCommitter.commit(
|
|
108
110
|
{
|
|
109
111
|
'_type' => 'COMPOSITION',
|
|
110
|
-
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.
|
|
112
|
+
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1',
|
|
111
113
|
'content' => [
|
|
112
|
-
{
|
|
114
|
+
{
|
|
115
|
+
'_type' => 'EVALUATION', 'archetype_node_id' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1',
|
|
116
|
+
'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1' } },
|
|
117
|
+
'data' => { '_type' => 'ITEM_TABLE', 'archetype_node_id' => 'at0001', 'rows' => [] }
|
|
118
|
+
}
|
|
113
119
|
]
|
|
114
120
|
},
|
|
115
|
-
uid: 'uid-
|
|
121
|
+
uid: 'uid-table-exec'
|
|
116
122
|
)
|
|
117
123
|
end
|
|
118
124
|
|
|
119
125
|
it 'still answers a query on another template, skipping the broken composition' do
|
|
120
126
|
rows = nil
|
|
121
127
|
|
|
122
|
-
expect { rows = described_class.execute(height_query).rows }.to output(/uid-
|
|
128
|
+
expect { rows = described_class.execute(height_query).rows }.to output(/uid-table-exec/).to_stderr
|
|
123
129
|
|
|
124
130
|
expect(rows).to eq([[170.0]])
|
|
125
131
|
end
|
|
126
132
|
end
|
|
133
|
+
|
|
134
|
+
# skoba/openehr-rails#45 (b) enhancement, end to end: values under
|
|
135
|
+
# INSTRUCTION.protocol / activities and inside a SECTION are reachable by
|
|
136
|
+
# AQL once the builder rebuilds those nodes. Same synthetic fixture as
|
|
137
|
+
# rm_object_builder_spec.rb.
|
|
138
|
+
describe 'INSTRUCTION and SECTION paths (#45)' do
|
|
139
|
+
before do
|
|
140
|
+
OpenehrRails::Rm::CompositionCommitter.commit(SyntheticReferralCanonicalHash.composition, uid: 'uid-referral-aql')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'reaches an ACTIVITY description leaf through i/activities[...]/description[...]' do
|
|
144
|
+
query = 'SELECT i/activities[at0001]/description[at0009]/items[at0121]/value/value ' \
|
|
145
|
+
'FROM EHR e CONTAINS COMPOSITION c ' \
|
|
146
|
+
"CONTAINS INSTRUCTION i[#{SyntheticReferralCanonicalHash::INSTRUCTION_ID}]"
|
|
147
|
+
|
|
148
|
+
expect(described_class.execute(query).rows).to eq([['Chest pain work-up']])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'reaches a protocol leaf through i/protocol[...]' do
|
|
152
|
+
query = 'SELECT i/protocol[at0008]/items[at0010]/value/value ' \
|
|
153
|
+
'FROM EHR e CONTAINS COMPOSITION c ' \
|
|
154
|
+
"CONTAINS INSTRUCTION i[#{SyntheticReferralCanonicalHash::INSTRUCTION_ID}]"
|
|
155
|
+
|
|
156
|
+
expect(described_class.execute(query).rows).to eq([['Synthetic Hospital']])
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'reaches an EVALUATION nested in a SECTION' do
|
|
160
|
+
query = 'SELECT ev/data[at0001]/items[at0002]/value/value ' \
|
|
161
|
+
'FROM EHR e CONTAINS COMPOSITION c ' \
|
|
162
|
+
"CONTAINS EVALUATION ev[#{SyntheticReferralCanonicalHash::EVALUATION_ID}]"
|
|
163
|
+
|
|
164
|
+
expect(described_class.execute(query).rows).to eq([['stable']])
|
|
165
|
+
end
|
|
166
|
+
end
|
|
127
167
|
end
|
|
@@ -268,6 +268,45 @@ describe OpenehrRails::Fhir::FshGenerator do
|
|
|
268
268
|
end
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
+
# skoba/openehr-rails#49, resolution shape (a) bug: an entry with no leaves
|
|
272
|
+
# at all produced an archetype `code` rule plus an empty `component` slicing
|
|
273
|
+
# header (and, for a non-Observation resource, invalid FHIR). Any 0-leaf
|
|
274
|
+
# entry is now skipped and reported, whatever its resource. Synthetic
|
|
275
|
+
# entries (stubbed FieldExtractor, invented ids), as for #33/#38.
|
|
276
|
+
describe '0-leaf entries' do
|
|
277
|
+
let(:real_entries) { OpenehrRails::Opt::FieldExtractor.new(template).entries }
|
|
278
|
+
let(:empty_observation) do
|
|
279
|
+
real_entries.first.merge(archetype_id: 'openEHR-EHR-OBSERVATION.synthetic_empty_test.v1',
|
|
280
|
+
concept: 'synthetic_empty_test', fields: [])
|
|
281
|
+
end
|
|
282
|
+
let(:empty_instruction) do
|
|
283
|
+
real_entries.first.merge(archetype_id: 'openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1',
|
|
284
|
+
rm_type: 'INSTRUCTION', concept: 'synthetic_empty_test', fields: [])
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
before do
|
|
288
|
+
extractor = instance_double(OpenehrRails::Opt::FieldExtractor,
|
|
289
|
+
entries: [empty_observation, empty_instruction, *real_entries])
|
|
290
|
+
allow(OpenehrRails::Opt::FieldExtractor).to receive(:new).with(template).and_return(extractor)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'emits nothing for them and keeps the real entries' do
|
|
294
|
+
expect(generator.to_fsh_files.keys).to eq(
|
|
295
|
+
%w[openehr-observation-height-v2 openehr-observation-body-weight-v2 openehr-observation-body-mass-index-v2]
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'reports each as an UnsupportedProfileError that says the entry has no leaves' do
|
|
300
|
+
generator.to_fsh_files
|
|
301
|
+
|
|
302
|
+
expect(generator.skipped.map(&:archetype_id)).to eq(
|
|
303
|
+
%w[openEHR-EHR-OBSERVATION.synthetic_empty_test.v1 openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1]
|
|
304
|
+
)
|
|
305
|
+
expect(generator.skipped.map(&:leaf_count)).to eq([0, 0])
|
|
306
|
+
expect(generator.skipped.map(&:message)).to all(include('no leaves'))
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
271
310
|
describe '#skipped' do
|
|
272
311
|
it 'is empty for an all-Observation template' do
|
|
273
312
|
generator.to_fsh_files
|
|
@@ -282,6 +282,45 @@ describe OpenehrRails::Fhir::ProfileGenerator do
|
|
|
282
282
|
end
|
|
283
283
|
end
|
|
284
284
|
|
|
285
|
+
# skoba/openehr-rails#49, resolution shape (a) bug: an entry with no leaves
|
|
286
|
+
# at all produced an archetype `code` rule plus an empty `component` slicing
|
|
287
|
+
# header (and, for a non-Observation resource, invalid FHIR). Any 0-leaf
|
|
288
|
+
# entry is now skipped and reported, whatever its resource. Synthetic
|
|
289
|
+
# entries (stubbed FieldExtractor, invented ids), as for #33/#38. JSON facade half.
|
|
290
|
+
describe '0-leaf entries' do
|
|
291
|
+
let(:real_entries) { OpenehrRails::Opt::FieldExtractor.new(template).entries }
|
|
292
|
+
let(:empty_observation) do
|
|
293
|
+
real_entries.first.merge(archetype_id: 'openEHR-EHR-OBSERVATION.synthetic_empty_test.v1',
|
|
294
|
+
concept: 'synthetic_empty_test', fields: [])
|
|
295
|
+
end
|
|
296
|
+
let(:empty_instruction) do
|
|
297
|
+
real_entries.first.merge(archetype_id: 'openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1',
|
|
298
|
+
rm_type: 'INSTRUCTION', concept: 'synthetic_empty_test', fields: [])
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
before do
|
|
302
|
+
extractor = instance_double(OpenehrRails::Opt::FieldExtractor,
|
|
303
|
+
entries: [empty_observation, empty_instruction, *real_entries])
|
|
304
|
+
allow(OpenehrRails::Opt::FieldExtractor).to receive(:new).with(template).and_return(extractor)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'emits nothing for them and keeps the real entries' do
|
|
308
|
+
expect(generator.profiles.map { |p| p[:id] }).to eq(
|
|
309
|
+
%w[openehr-observation-height-v2 openehr-observation-body-weight-v2 openehr-observation-body-mass-index-v2]
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'reports each as an UnsupportedProfileError that says the entry has no leaves' do
|
|
314
|
+
generator.profiles
|
|
315
|
+
|
|
316
|
+
expect(generator.skipped.map(&:archetype_id)).to eq(
|
|
317
|
+
%w[openEHR-EHR-OBSERVATION.synthetic_empty_test.v1 openEHR-EHR-INSTRUCTION.synthetic_empty_test.v1]
|
|
318
|
+
)
|
|
319
|
+
expect(generator.skipped.map(&:leaf_count)).to eq([0, 0])
|
|
320
|
+
expect(generator.skipped.map(&:message)).to all(include('no leaves'))
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
285
324
|
describe '#skipped' do
|
|
286
325
|
it 'is empty for an all-Observation template' do
|
|
287
326
|
generator.profiles
|
|
@@ -65,6 +65,26 @@ describe OpenehrRails::Opt::FieldExtractor do
|
|
|
65
65
|
expect(diagnostic_status[:value_set_uri]).to be_nil
|
|
66
66
|
expect(diagnostic_status[:code_list]).not_to be_empty
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
# skoba/openehr-rails#52, resolution shape (b) enhancement: the value
|
|
70
|
+
# constraint's alternative RM types travel with the field, in OPT order,
|
|
71
|
+
# so a host app can tell an OR-constrained coded text (DV_TEXT allowed)
|
|
72
|
+
# from a coded-only one. rm_type selection is unchanged. Values measured
|
|
73
|
+
# on the fixture before the spec was written.
|
|
74
|
+
describe 'rm_type_alternatives' do
|
|
75
|
+
it 'lists every alternative of an OR-constrained value in OPT order' do
|
|
76
|
+
expect(field_for(fields, archetype_id, 'at0002')[:rm_type_alternatives]).to eq(%w[DV_TEXT DV_CODED_TEXT])
|
|
77
|
+
expect(field_for(fields, archetype_id, 'at0073')[:rm_type_alternatives]).to eq(%w[DV_CODED_TEXT DV_TEXT])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'lists the single type of a plain value' do
|
|
81
|
+
expect(field_for(fields, archetype_id, 'at0077')[:rm_type_alternatives]).to eq(%w[DV_DATE_TIME])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'leaves the chosen rm_type as before' do
|
|
85
|
+
expect(field_for(fields, archetype_id, 'at0002')[:rm_type]).to eq('DV_CODED_TEXT')
|
|
86
|
+
end
|
|
87
|
+
end
|
|
68
88
|
end
|
|
69
89
|
|
|
70
90
|
describe 'a hand-built template without terminology objects' do
|
|
@@ -93,7 +113,7 @@ describe OpenehrRails::Opt::FieldExtractor do
|
|
|
93
113
|
|
|
94
114
|
# enhancement: additive binding keys remain safe for lightweight template doubles.
|
|
95
115
|
expect(described_class.new(template).fields.first)
|
|
96
|
-
.to include(value_set_uri: nil, code_bindings: [])
|
|
116
|
+
.to include(value_set_uri: nil, code_bindings: [], rm_type_alternatives: %w[DV_TEXT])
|
|
97
117
|
end
|
|
98
118
|
end
|
|
99
119
|
end
|
|
@@ -77,21 +77,100 @@ describe OpenehrRails::Rm::RmObjectBuilder do
|
|
|
77
77
|
# skoba/openehr-rails#44 (a) bug: a node type the graph stores but
|
|
78
78
|
# TYPE_CLASSES lacks used to surface as `NoMethodError: undefined method
|
|
79
79
|
# 'new' for nil`. The typed error names what the caller needs to act on.
|
|
80
|
+
# Since #45 SECTION is buildable, so the unsupported type here is an
|
|
81
|
+
# ITEM_TABLE as an EVALUATION's data (ruling condition (c)).
|
|
80
82
|
it 'raises UnsupportedRmTypeError, naming composition and node, for an rm_type outside TYPE_CLASSES' do
|
|
81
83
|
composition = OpenehrRails::Rm::CompositionCommitter.commit(
|
|
82
84
|
{
|
|
83
85
|
'_type' => 'COMPOSITION',
|
|
84
|
-
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.
|
|
86
|
+
'archetype_node_id' => 'openEHR-EHR-COMPOSITION.synthetic_table_test.v1',
|
|
85
87
|
'content' => [
|
|
86
|
-
{
|
|
88
|
+
{
|
|
89
|
+
'_type' => 'EVALUATION', 'archetype_node_id' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1',
|
|
90
|
+
'archetype_details' => { 'archetype_id' => { 'value' => 'openEHR-EHR-EVALUATION.synthetic_table_test.v1' } },
|
|
91
|
+
'data' => { '_type' => 'ITEM_TABLE', 'archetype_node_id' => 'at0001', 'rows' => [] }
|
|
92
|
+
}
|
|
87
93
|
]
|
|
88
94
|
},
|
|
89
|
-
uid: 'uid-
|
|
95
|
+
uid: 'uid-table-builder'
|
|
90
96
|
)
|
|
91
97
|
|
|
92
98
|
expect { composition.to_rm }.to raise_error(OpenehrRails::Rm::UnsupportedRmTypeError) { |error|
|
|
93
|
-
expect(error.message).to include('uid-
|
|
94
|
-
expect(error.rm_type).to eq('
|
|
99
|
+
expect(error.message).to include('uid-table-builder', 'ITEM_TABLE', 'data[at0001]')
|
|
100
|
+
expect(error.rm_type).to eq('ITEM_TABLE')
|
|
95
101
|
}
|
|
96
102
|
end
|
|
103
|
+
|
|
104
|
+
# skoba/openehr-rails#45, resolution shape (b) enhancement: SECTION,
|
|
105
|
+
# INSTRUCTION and ACTIVITY nodes -- persisted by GraphBuilder all along --
|
|
106
|
+
# are rebuilt as RM objects. Fixture: spec/support/synthetic_referral_canonical_hash.rb
|
|
107
|
+
# (synthetic; design authority docs/design/rm-object-builder-section-instruction-plan.md
|
|
108
|
+
# section 3). Red before #45: UnsupportedRmTypeError on the SECTION.
|
|
109
|
+
describe 'SECTION, INSTRUCTION and ACTIVITY (#45)' do
|
|
110
|
+
let(:fixture) { SyntheticReferralCanonicalHash }
|
|
111
|
+
let(:rm) do
|
|
112
|
+
OpenehrRails::Rm::CompositionCommitter.commit(fixture.composition, uid: 'uid-referral').to_rm
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'rebuilds a SECTION with its items' do
|
|
116
|
+
section = rm.content.first
|
|
117
|
+
|
|
118
|
+
expect(section).to be_a(OpenEHR::RM::Composition::Content::Navigation::Section)
|
|
119
|
+
expect(section.archetype_node_id).to eq(fixture::SECTION_ID)
|
|
120
|
+
expect(section.items.size).to eq(1)
|
|
121
|
+
expect(section.items.first).to be_a(OpenEHR::RM::Composition::Content::Entry::Evaluation)
|
|
122
|
+
expect(section.items.first.data.items.first.value.value).to eq('stable')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'rebuilds an INSTRUCTION with entry defaults, narrative, protocol and activities' do
|
|
126
|
+
instruction = rm.content.last
|
|
127
|
+
|
|
128
|
+
expect(instruction).to be_a(OpenEHR::RM::Composition::Content::Entry::Instruction)
|
|
129
|
+
expect(instruction.language.code_string).to eq(OpenehrRails.default_language)
|
|
130
|
+
expect(instruction.narrative.value).to eq('Refer to cardiology')
|
|
131
|
+
expect(instruction.protocol.items.first.value.value).to eq('Synthetic Hospital')
|
|
132
|
+
expect(instruction.activities.size).to eq(1)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'rebuilds an ACTIVITY with description, timing and the injected action_archetype_id' do
|
|
136
|
+
activity = rm.content.last.activities.first
|
|
137
|
+
|
|
138
|
+
expect(activity).to be_a(OpenEHR::RM::Composition::Content::Entry::Activity)
|
|
139
|
+
expect(activity.archetype_node_id).to eq('at0001')
|
|
140
|
+
expect(activity.description.items.first.value.value).to eq('Chest pain work-up')
|
|
141
|
+
expect(activity.timing).to be_a(OpenEHR::RM::DataTypes::Encapsulated::DvParsable)
|
|
142
|
+
expect(activity.timing.value).to eq('R1')
|
|
143
|
+
expect(activity.timing.formalism).to eq('timing')
|
|
144
|
+
# Approximation (ruling condition (a)): the graph does not carry
|
|
145
|
+
# action_archetype_id yet, so the RM's "any ACTION archetype" pattern is
|
|
146
|
+
# injected; phase B persists the real value.
|
|
147
|
+
expect(activity.action_archetype_id).to eq('/.*/')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Approximation (ruling condition (a)): narrative is mandatory on the RM
|
|
151
|
+
# INSTRUCTION; when the graph has no narrative row, the node's name stands
|
|
152
|
+
# in. Phase B persists it as a column.
|
|
153
|
+
it 'injects the node name as narrative when the graph has none' do
|
|
154
|
+
composition = OpenehrRails::Rm::CompositionCommitter.commit(
|
|
155
|
+
fixture.composition(content: [fixture.instruction(narrative: false)]), uid: 'uid-referral-no-narrative'
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
expect(composition.to_rm.content.first.narrative.value).to eq('Request')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'builds a SECTION without items as items nil (the RM rejects an empty list)' do
|
|
162
|
+
composition = OpenehrRails::Rm::CompositionCommitter.commit(
|
|
163
|
+
fixture.composition(content: [fixture.section([])]), uid: 'uid-referral-empty-section'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
expect(composition.to_rm.content.first.items).to be_nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Ruling condition (b): restoring `protocol` for every CARE_ENTRY must not
|
|
170
|
+
# change what an entry without a protocol node rebuilds to.
|
|
171
|
+
# Regression pin -- this was already true before #45.
|
|
172
|
+
it 'leaves protocol nil on an OBSERVATION that has no protocol node' do
|
|
173
|
+
expect(BmiCalculation.create!(height: 170.0).rm_graph.to_rm.content.first.protocol).to be_nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
97
176
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fixture kind: **synthetic** (hand-authored, not derived from any real
|
|
4
|
+
# artifact). Design authority: docs/design/rm-object-builder-section-instruction-plan.md
|
|
5
|
+
# section 3 (skoba/openehr-rails#45). Every archetype id is self-evidently
|
|
6
|
+
# invented (`synthetic_*_test`); the at-codes mirror the *shape* of
|
|
7
|
+
# jp_referral's service_request (activities[at0001]/description[at0009],
|
|
8
|
+
# protocol[at0008]) so the AQL paths in the specs read like the real ones,
|
|
9
|
+
# but no real canonical JSON with SECTION/INSTRUCTION exists in this repo --
|
|
10
|
+
# anlage's hand-mapped composition lives outside it.
|
|
11
|
+
#
|
|
12
|
+
# Shape: COMPOSITION whose content is a SECTION holding one EVALUATION, and
|
|
13
|
+
# an INSTRUCTION with a narrative, a protocol ITEM_TREE and one ACTIVITY
|
|
14
|
+
# (description ITEM_TREE + DV_PARSABLE timing). Everything GraphBuilder
|
|
15
|
+
# accepts today; before #45 RmObjectBuilder could rebuild none of it.
|
|
16
|
+
module SyntheticReferralCanonicalHash
|
|
17
|
+
COMPOSITION_ID = 'openEHR-EHR-COMPOSITION.synthetic_referral_test.v1'
|
|
18
|
+
SECTION_ID = 'openEHR-EHR-SECTION.synthetic_details_test.v1'
|
|
19
|
+
EVALUATION_ID = 'openEHR-EHR-EVALUATION.synthetic_summary_test.v1'
|
|
20
|
+
INSTRUCTION_ID = 'openEHR-EHR-INSTRUCTION.synthetic_request_test.v1'
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def dv_text(value)
|
|
25
|
+
{ '_type' => 'DV_TEXT', 'value' => value }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def element(node_id, value)
|
|
29
|
+
{ '_type' => 'ELEMENT', 'archetype_node_id' => node_id, 'name' => dv_text(node_id), 'value' => value }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def item_tree(node_id, items)
|
|
33
|
+
{ '_type' => 'ITEM_TREE', 'archetype_node_id' => node_id, 'name' => dv_text(node_id), 'items' => items }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def archetyped(id)
|
|
37
|
+
{ '_type' => 'ARCHETYPED', 'archetype_id' => { 'value' => id }, 'rm_version' => '1.0.4' }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def evaluation
|
|
41
|
+
{
|
|
42
|
+
'_type' => 'EVALUATION', 'archetype_node_id' => EVALUATION_ID,
|
|
43
|
+
'archetype_details' => archetyped(EVALUATION_ID), 'name' => dv_text('Summary'),
|
|
44
|
+
'data' => item_tree('at0001', [element('at0002', dv_text('stable'))])
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def activity
|
|
49
|
+
{
|
|
50
|
+
'_type' => 'ACTIVITY', 'archetype_node_id' => 'at0001', 'name' => dv_text('Request'),
|
|
51
|
+
'description' => item_tree('at0009', [element('at0121', dv_text('Chest pain work-up'))]),
|
|
52
|
+
'timing' => { '_type' => 'DV_PARSABLE', 'value' => 'R1', 'formalism' => 'timing' }
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# narrative: pass false to omit the narrative (the injected-default case).
|
|
57
|
+
def instruction(narrative: 'Refer to cardiology')
|
|
58
|
+
hash = {
|
|
59
|
+
'_type' => 'INSTRUCTION', 'archetype_node_id' => INSTRUCTION_ID,
|
|
60
|
+
'archetype_details' => archetyped(INSTRUCTION_ID), 'name' => dv_text('Request'),
|
|
61
|
+
'protocol' => item_tree('at0008', [element('at0010', dv_text('Synthetic Hospital'))]),
|
|
62
|
+
'activities' => [activity]
|
|
63
|
+
}
|
|
64
|
+
hash['narrative'] = dv_text(narrative) if narrative
|
|
65
|
+
hash
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def section(items = [evaluation])
|
|
69
|
+
{ '_type' => 'SECTION', 'archetype_node_id' => SECTION_ID, 'name' => dv_text('Details'), 'items' => items }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def composition(content: [section, instruction])
|
|
73
|
+
{
|
|
74
|
+
'_type' => 'COMPOSITION', 'archetype_node_id' => COMPOSITION_ID,
|
|
75
|
+
'archetype_details' => archetyped(COMPOSITION_ID).merge('template_id' => { 'value' => 'synthetic_referral_test' }),
|
|
76
|
+
'name' => dv_text('Synthetic referral'),
|
|
77
|
+
'content' => content
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openehr-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shinji KOBAYASHI
|
|
@@ -346,6 +346,7 @@ files:
|
|
|
346
346
|
- spec/generators/templates/openEHR-EHR-OBSERVATION.blood_pressure.v1.adl
|
|
347
347
|
- spec/models/openehr_template_spec.rb
|
|
348
348
|
- spec/openehr_rails/aql/dataset_adapter_spec.rb
|
|
349
|
+
- spec/openehr_rails/aql/executor_execute_demo_queries_spec.rb
|
|
349
350
|
- spec/openehr_rails/aql/executor_spec.rb
|
|
350
351
|
- spec/openehr_rails/aql/model_api_spec.rb
|
|
351
352
|
- spec/openehr_rails/aql/query_validator_spec.rb
|
|
@@ -383,6 +384,7 @@ files:
|
|
|
383
384
|
- spec/openehr_rails/template_uploader_spec.rb
|
|
384
385
|
- spec/spec_helper.rb
|
|
385
386
|
- spec/support/active_record.rb
|
|
387
|
+
- spec/support/synthetic_referral_canonical_hash.rb
|
|
386
388
|
- spec/templates/bmi_calculation_without_uid.opt
|
|
387
389
|
- spec/templates/lab_result_report_reduced.opt
|
|
388
390
|
- spec/templates/problem_list.opt
|