diff --git a/.github/workflows/ci-r4b.yml b/.github/workflows/ci-r4b.yml
new file mode 100644
index 00000000..585ea478
--- /dev/null
+++ b/.github/workflows/ci-r4b.yml
@@ -0,0 +1,86 @@
+name: CI R4B
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - 'master'
+ pull_request:
+
+jobs:
+ build:
+ if: github.repository == 'incendilabs/plan-executor'
+
+ runs-on: ubuntu-24.04
+ steps:
+ -
+ name: Checkout plan-executor
+ uses: actions/checkout@v7
+ -
+ name: Build plan-executor Docker image
+ run: docker build -t incendi/plan_executor:latest .
+ -
+ name: Checkout Spark
+ uses: actions/checkout@v7
+ with:
+ repository: FirelyTeam/spark
+ ref: master
+ path: spark
+ -
+ name: Build Spark R4B Docker image
+ run: docker build spark --file spark/.docker/linux/Spark.R4B.Dockerfile
+ --tag sparkfhir/spark:r4b-latest
+ -
+ name: Build Mongo R4B Docker image
+ run: docker build spark --file spark/.docker/linux/Mongo.R4B.Dockerfile
+ --tag sparkfhir/mongo:r4b-latest
+ -
+ name: Run tests
+ run: |
+ mkdir -p logs html_summaries json_results
+ docker compose -f docker-compose.yml -f docker-compose-r4b.yml up -d spark
+ docker compose -f docker-compose.yml -f docker-compose-r4b.yml run --rm --no-deps plan_executor ./execute_all.sh 'http://spark:8080/fhir' r4b 'html|json|stdout'
+ docker compose -f docker-compose.yml -f docker-compose-r4b.yml logs spark > logs/backend.log
+ -
+ name: Combine test results
+ if: ${{ always() }}
+ run: ./combine-test-results.sh json_results annotations.json
+ -
+ name: Attach test results
+ if: github.event_name != 'pull_request'
+ uses: yuzutech/annotations-action@v0.6.0
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
+ input: annotations.json
+ -
+ name: Archive logs
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: logs-r4b-${{ github.sha }}
+ path: logs/*.log*
+ -
+ name: Archive test reports
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: html_summaries-r4b-${{ github.sha }}
+ path: html_summaries/**/*.html
+ -
+ name: Archive JSON results
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: json_results-r4b-${{ github.sha }}
+ path: json_results/**/*.json
+ -
+ name: Archive annotations file
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: annotations-r4b-${{ github.sha }}
+ path: annotations.json
+ -
+ name: Cleanup
+ if: ${{ always() }}
+ run: docker compose -f docker-compose.yml -f docker-compose-r4b.yml down
diff --git a/.github/workflows/ci-stu3.yml b/.github/workflows/ci-stu3.yml
index 018594cf..731c0b94 100644
--- a/.github/workflows/ci-stu3.yml
+++ b/.github/workflows/ci-stu3.yml
@@ -14,18 +14,33 @@ jobs:
runs-on: ubuntu-24.04
steps:
-
- name: Checkout repo
+ name: Checkout plan-executor
uses: actions/checkout@v7
-
- name: Build docker image
+ name: Build plan-executor Docker image
run: docker build -t incendi/plan_executor:latest .
+ -
+ name: Checkout Spark
+ uses: actions/checkout@v7
+ with:
+ repository: FirelyTeam/spark
+ ref: master
+ path: spark
+ -
+ name: Build Spark STU3 Docker image
+ run: docker build spark --file spark/.docker/linux/Spark.STU3.Dockerfile
+ --tag sparkfhir/spark:stu3-latest
+ -
+ name: Build Mongo STU3 Docker image
+ run: docker build spark --file spark/.docker/linux/Mongo.STU3.Dockerfile
+ --tag sparkfhir/mongo:stu3-latest
-
name: Run tests
run: |
mkdir -p logs html_summaries json_results
docker compose -f docker-compose-stu3.yml up -d spark
- docker compose run --rm --no-deps plan_executor ./execute_all.sh 'http://spark:8080/fhir' stu3 'html|json|stdout'
- docker compose logs spark > logs/backend.log
+ docker compose -f docker-compose-stu3.yml run --rm --no-deps plan_executor ./execute_all.sh 'http://spark:8080/fhir' stu3 'html|json|stdout'
+ docker compose -f docker-compose-stu3.yml logs spark > logs/backend.log
-
name: Combine test results
if: ${{ always() }}
@@ -42,30 +57,30 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
- name: logs-r4-${{ github.sha }}
+ name: logs-stu3-${{ github.sha }}
path: logs/*.log*
-
name: Archive test reports
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
- name: html_summaries-r4-${{ github.sha }}
+ name: html_summaries-stu3-${{ github.sha }}
path: html_summaries/**/*.html
-
name: Archive JSON results
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
- name: json_results-r4-${{ github.sha }}
+ name: json_results-stu3-${{ github.sha }}
path: json_results/**/*.json
-
name: Archive annotations file
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
- name: annotations-r4-${{ github.sha }}
+ name: annotations-stu3-${{ github.sha }}
path: annotations.json
-
name: Cleanup
if: ${{ always() }}
- run: docker compose down
\ No newline at end of file
+ run: docker compose -f docker-compose-stu3.yml down
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a215af51..eac36812 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,11 +14,26 @@ jobs:
runs-on: ubuntu-24.04
steps:
-
- name: Checkout repo
+ name: Checkout plan-executor
uses: actions/checkout@v7
-
- name: Build docker image
+ name: Build plan-executor Docker image
run: docker build -t incendi/plan_executor:latest .
+ -
+ name: Checkout Spark
+ uses: actions/checkout@v7
+ with:
+ repository: FirelyTeam/spark
+ ref: master
+ path: spark
+ -
+ name: Build Spark R4 Docker image
+ run: docker build spark --file spark/.docker/linux/Spark.R4.Dockerfile
+ --tag sparkfhir/spark:r4-latest
+ -
+ name: Build Mongo R4 Docker image
+ run: docker build spark --file spark/.docker/linux/Mongo.R4.Dockerfile
+ --tag sparkfhir/mongo:r4-latest
-
name: Run tests
run: |
@@ -68,4 +83,4 @@ jobs:
-
name: Cleanup
if: ${{ always() }}
- run: docker compose down
\ No newline at end of file
+ run: docker compose down
diff --git a/Gemfile b/Gemfile
index b19e1a67..2b768960 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,8 +2,8 @@ source "https://rubygems.org"
gem 'fhir_dstu2_models', git: 'https://github.com/incendilabs/fhir_dstu2_models.git'
gem 'fhir_stu3_models', git: 'https://github.com/incendilabs/fhir_stu3_models.git'
-gem 'fhir_client', git: 'https://github.com/incendilabs/fhir_client.git'
-gem 'fhir_models', '~> 4.1'
+gem 'fhir_client', git: 'https://github.com/incendilabs/fhir_client.git', branch: 'master'
+gem 'fhir_models', git: 'https://github.com/incendilabs/fhir_models.git', branch: 'master'
gem 'activesupport', '>= 6'
gem 'pry', '~> 0.15.0'
gemspec
diff --git a/Gemfile.lock b/Gemfile.lock
index cdbe96b9..d25799ad 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,8 +1,9 @@
GIT
remote: https://github.com/incendilabs/fhir_client.git
- revision: a2298086b051f18595dcfe5768f63545f2a271bb
+ revision: 79026641f9b2ac7cf30bc27a3528e505d34c67e8
+ branch: master
specs:
- fhir_client (4.0.4)
+ fhir_client (5.0.0)
activesupport (>= 3)
addressable (>= 2.9.0)
fhir_dstu2_models (>= 1.0.10)
@@ -24,6 +25,17 @@ GIT
mime-types (>= 3.0)
nokogiri (>= 1.10.4)
+GIT
+ remote: https://github.com/incendilabs/fhir_models.git
+ revision: a143d2e21d0253b33fdaeb17e2d152ad656c9a3e
+ branch: master
+ specs:
+ fhir_models (4.1.0)
+ bcp47 (>= 0.3)
+ date_time_precision (>= 0.8)
+ mime-types (>= 3.0)
+ nokogiri (>= 1.10.4)
+
GIT
remote: https://github.com/incendilabs/fhir_stu3_models.git
revision: 71db01196b6cafe2310498135849cae356fe6f44
@@ -90,11 +102,6 @@ GEM
logger
faraday-net_http (3.4.4)
net-http (~> 0.5)
- fhir_models (4.3.0)
- bcp47 (>= 0.3)
- date_time_precision (>= 0.8)
- mime-types (>= 3.0)
- nokogiri (>= 1.11.4)
hashdiff (1.2.1)
http-accept (1.7.0)
http-cookie (1.1.4)
@@ -182,7 +189,7 @@ DEPENDENCIES
awesome_print
fhir_client!
fhir_dstu2_models!
- fhir_models (~> 4.1)
+ fhir_models!
fhir_stu3_models!
plan_executor!
pry (~> 0.15.0)
diff --git a/R4B.md b/R4B.md
new file mode 100644
index 00000000..1cf74249
--- /dev/null
+++ b/R4B.md
@@ -0,0 +1,610 @@
+# R4B Support Action Points
+
+## Design Rules
+
+- Treat `:r4` and `:r4b` as distinct harness versions.
+- Never allow R4B to resolve through implicit R4 fallback.
+- Shared R4/R4B behavior is allowed only through explicit compatibility annotations.
+- Keep R4 models at the existing top-level `FHIR::*` namespace for backward compatibility and expose R4B models through the required `FHIR::R4B::*` namespace.
+- Require an explicit version at every client, task, structure, fixture, and
+ resource-generator boundary. Omitted and unknown versions must fail fast.
+- `fhir_client` may use the explicit sentinel `fhir_version: :auto` only for
+ CapabilityStatement discovery. Versioned resource operations must remain
+ unavailable until discovery selects a concrete version.
+- Keep version annotations explicit, even for resources that are normative or unchanged across R4 and R4B.
+- Add R4B model support to `fhir_models`; do not introduce a separate R4B model gem by default.
+- Keep the registry of versions understood by the harness separate from the versions supported by each test suite.
+
+## Related Repositories
+
+- `git@github.com:incendilabs/fhir_client.git`, local path `../fhir_client`
+- `git@github.com:incendilabs/fhir_models.git`, local path `../fhir_models`
+- `git@github.com:incendilabs/fhir_dstu2_models.git`, local path `../fhir_dstu2_models`
+- `git@github.com:incendilabs/fhir_stu3_models.git`, local path `../fhir_stu3_models`
+
+R4B feature work is expected in `fhir_models`, `fhir_client`, and this repository. The DSTU2 and STU3 model repositories remain in scope as regression dependencies, but no R4B feature changes are expected in them.
+
+## Implementation Status
+
+- `fhir_models` now has namespace-aware infrastructure, generated
+ `FHIR::R4B` models, checked-in R4B runtime definitions, documented generation,
+ and R4B XML schema validation. These changes are split across commits
+ `cf7f5d5a`, `03059207`, `90cd55fd`, `67738dfe`, and `444f74f7`.
+- `fhir_client` now has explicit R4B routing and CapabilityStatement version
+ detection in commit `7bde8ed2`.
+- Strict client versioning is a separate breaking change in commit `75d3c33`:
+ `FHIR::Client.new` requires `fhir_version:`, direct R4 remains
+ `fhir_version: :r4`, and automatic discovery must be requested with
+ `fhir_version: :auto`.
+- `plan-executor` now has a central version registry, version-aware resource
+ routing, a generated R4B structure index, corrected version-specific fixture
+ lookup, and explicit suite compatibility annotations. These changes are split
+ across commits `17537c9a`, `2e7a759`, `d70c01c`, `12e10e8`, and `a2b1482`.
+- Strict harness versioning and namespace-explicit generator helpers are in
+ commit `16347df`.
+- All 12 executable Ruby suites that already support R4 now explicitly support
+ R4B in commit `826433b`. STU3-only, DSTU2-only, TestScript, and explicitly
+ unsupported suites retain their existing annotations.
+- The R4B PackagedProductDefinition fixture defect is resolved. Empty generated
+ `CodeableReference` values now receive a same-namespace text concept, so
+ recursive `containedItem.item` elements remain present when serialized. The
+ fix is commit `49e713f`.
+- The nondeterministic R4B Questionnaire defect is resolved. Generated codes
+ are selected from concrete expansion entries while abstract and inactive
+ entries remain available in the checked-in terminology definitions. The fix
+ is commit `84fa7c4`.
+- `plan-executor` now resolves `fhir_models` and `fhir_client` from their merged
+ `origin/master` branches, with the exact resolved revisions retained in
+ `Gemfile.lock`.
+- Remaining integration work consists of completing the STU3 and DSTU2
+ cross-version regression matrix.
+
+## Baseline And Dependency Actions
+
+- Reconcile the local `../fhir_models` checkout with the version currently resolved by this repository before implementing R4B. The local checkout identifies itself as gem version `4.1.0` with FHIR `4.0.1` definitions, while this repository currently locks released `fhir_models` version `4.3.0`.
+- Do not confuse the `fhir_models` gem version with the FHIR specification version. Record both independently.
+- Pin the official FHIR R4B `4.3.0` JSON definitions archive, JSON ValueSet
+ expansion Bundle, and XML schema archive used for generation. Generate models
+ and runtime definitions with
+ `bundle exec rake "fhir:generate_r4b[path/to/r4b-definitions.json.zip,path/to/expansions.json]"`.
+ Generate the XML schema set with
+ `bundle exec rake "fhir:generate_r4b_schema[path/to/r4b-fhir-all-xsd.zip]"`.
+ Download the artifacts from the official HL7
+ [`definitions.json.zip`](https://www.hl7.org/fhir/R4B/definitions.json.zip) and
+ [`expansions.json`](https://www.hl7.org/fhir/R4B/expansions.json) endpoints, and
+ the [`fhir-all-xsd.zip`](https://www.hl7.org/fhir/R4B/fhir-all-xsd.zip)
+ endpoint.
+ The pinned SHA-256 values are
+ `a2793a06853c2d4540db8a72fc1c6d972528b01d113c2bb70ae2d80dc062e963`
+ for the definitions archive and
+ `fe10ca33f0de85c16b367cb57092076d7e2fbd7aff6479c8862a32bd227e3b07`
+ for the expansion Bundle, and
+ `3528d4ff44c69f2908d6159367d58b9d12fb41a48d1be3ec897947129696e6b4`
+ for the XML schema archive.
+- Define the local cross-repository development setup, using temporary `path:` dependencies or equivalent local wiring so changes in `../fhir_models` and `../fhir_client` are exercised by this repository.
+- Define the release and dependency update order: `fhir_models`, then `fhir_client`, then `plan-executor`.
+- Complete: resolve `fhir_models` and `fhir_client` from their merged master
+ branches and retain their immutable resolved revisions in `Gemfile.lock`.
+
+## Model Definition Architecture
+
+### R4 Baseline
+
+- R4 runtime definitions remain under `lib/fhir_models/definitions/` in
+ `fhir_models`.
+- R4 stores separate preprocessed files for StructureDefinitions, ValueSets,
+ expansions, XML schemas, and version metadata.
+- `FHIR::Definitions` reads the individual JSON files directly. R4 XML
+ validation reads the XSD files under `lib/fhir_models/definitions/schema/`.
+- The generated R4 Ruby models remain separate under `lib/fhir_models/fhir/`.
+- Preserve this layout and the existing top-level `FHIR::Definitions` API for
+ backward compatibility.
+
+### R4B Runtime Definitions
+
+- Generated R4B Ruby models and metadata are stored under
+ `lib/fhir_models/r4b/`.
+- R4B runtime definitions follow the established R4 directory pattern under
+ `lib/fhir_models/definitions/r4b/`, with separate `structures/`,
+ `valuesets/`, and `schema/` directories plus `version.info`.
+- `FHIR::Definitions` and `FHIR::R4B::Definitions` use the same configurable,
+ directory-backed provider. R4 remains configured against
+ `lib/fhir_models/definitions/`, while R4B is configured against
+ `lib/fhir_models/definitions/r4b/` and constructs R4B model objects.
+- Definition bundles are parsed lazily and cached in memory. The R4B provider
+ verifies `version.info` against FHIR version `4.3.0` before loading them.
+- StructureDefinition objects returned by the provider must be
+ `FHIR::R4B::StructureDefinition` instances. Model binding, reference, and
+ StructureDefinition validation must select definitions from the owning model
+ namespace and must never fall back implicitly from R4B to R4.
+- The checked-in generated files contain datatype and resource
+ StructureDefinitions, profiles, extensions, search parameters, ValueSets,
+ expansions, and version metadata.
+- The shared provider preserves the existing R4 Definitions API for both
+ versions, including raw `valuesets`, raw `expansions`, terminology lookup,
+ display lookup, and dynamic `get_profile_class` behavior.
+
+### Generation And Repository Policy
+
+- Official HL7 `definitions.json.zip` and `expansions.json` files are generation
+ inputs. Pin their URLs and SHA-256 checksums, but do not check the downloaded
+ source artifacts into the repository.
+- Generate and check in the R4B Ruby models and the derived, preprocessed
+ runtime definition files. Normal use of the `fhir_models` gem must not require
+ a network connection or local copies of the HL7 source downloads.
+- Generation must be deterministic. Repeated generation from the pinned inputs
+ must produce byte-identical Ruby models and runtime definition output.
+- Keep the generated definition files as text. Git already compresses repository
+ objects, while text files retain useful diffs and delta compression that a
+ generated gzip index would prevent.
+
+### Harness Structure Index
+
+- `lib/FHIR_structure_r4b.json` is generated from the pinned R4B
+ `definitions.json.zip` input and checked into `plan-executor`.
+- Regenerate it with
+ `bundle exec rake "crucible:generate_r4b_structure[path/to/r4b-definitions.json.zip]"`.
+ The task verifies the pinned source checksum before reading
+ `profiles-resources.json` from the archive.
+- Resource names and categories come from concrete specialization
+ StructureDefinitions. The existing R4 structure index supplies only the
+ non-resource hierarchy and category template; it is not the source of the
+ R4B resource list.
+- The official R4B StructureDefinitions omit category extensions for
+ `ResearchDefinition` and `ResearchElementDefinition`. The generator assigns
+ both explicitly to `Specialized.Evidence-Based Medicine`.
+- The downloaded definitions archive remains an untracked generation input.
+ Repeated generation from the pinned input must produce byte-identical output.
+
+### XML Schema Status
+
+- The generated R4B JSON definition bundles do not contain the R4B XML XSD
+ schema set; the schemas come from the separately pinned official archive.
+- `FHIR::R4B::Xml.validate` uses the R4B `4.3.0` schema set and does not reuse
+ the R4 `4.0.1` schema directory.
+- Generated, preprocessed R4B schemas are checked in under
+ `lib/fhir_models/definitions/r4b/schema/`, parallel to R4. Use the existing
+ `FHIR::Boot::Preprocess.pre_process_schema` implementation rather than adding
+ a separate schema generator or runtime schema abstraction.
+- The original downloaded HL7 schema archive remains a checksum-pinned
+ generation input and is not checked into the repository.
+
+## Compatibility Annotation Actions
+
+- Introduce one authoritative registry of FHIR versions understood by the harness. This registry may include `:r4b`, but it must not imply that every suite supports R4B.
+- Keep `supported_versions` as the explicit suite compatibility annotation.
+- `BaseTest#supported_versions` defaults to an empty list. Do not add `:r4b` or
+ any other implicit compatibility to that default.
+- Add `:r4b` explicitly to each compatible suite; do not infer R4B support from
+ R4 support at runtime.
+- All executable suites now have explicit annotations. Seven suites that
+ previously relied on the base default explicitly preserve their existing
+ compatibility. All 12 suites that support R4 now also declare R4B explicitly.
+ No suite gains R4B support implicitly.
+- Update resource-based suite enumeration so it intersects known versions, the suite's declared `supported_versions`, and resources available in that version. It must not overwrite a suite's declared compatibility.
+- Ensure suite listing and suite execution use the same compatibility decision.
+- Keep TestScripts STU3-only unless separate R4B TestScripts and an R4B TestScript parser are deliberately added.
+
+## Implementation Actions
+
+### 1. R4B Models In `fhir_models`
+
+- Refactor model generation so the output directory and Ruby namespace are version-aware instead of being hard-coded to the top-level `FHIR` namespace.
+- Refactor JSON and XML deserialization, resource detection, validation, metadata, definitions, and schema lookup so they resolve classes and resource lists through the selected model namespace.
+- Preserve the current top-level R4 public API while adding generated R4B classes, metadata, parsers, validation, definitions, schemas, and resource lists under `FHIR::R4B`.
+- Ensure embedded resources, contained resources, Bundle entries, complex data types, and generated references remain in the R4B namespace.
+- Add model-level tests for generation, parsing, serialization, validation, and namespace purity.
+
+### 2. R4B Routing In `fhir_client`
+
+- Add `use_r4b` and route `:r4b` resource lookup, parsing, request replay, response validation, transactions, operations, and capability statements through `FHIR::R4B`.
+- Require `fhir_version:` when constructing a client. Do not retain R4 as a
+ constructor default.
+- Permit `fhir_version: :auto` only as an explicit discovery mode. Metadata may
+ establish a concrete version, but resource operations must reject `:auto`
+ until that has happened.
+- Make JSON and XML reply parsing select R4B explicitly instead of allowing the existing non-DSTU2/STU3 fallback to use R4.
+- Map CapabilityStatement `fhirVersion` values explicitly: `4.0.x` to `:r4` and `4.3.x` to `:r4b`.
+- Do not classify every version beginning with `4` as R4. Unknown FHIR 4.x releases must be reported as unsupported rather than silently parsed as R4.
+- Test both explicit `use_r4b` selection and automatic version detection.
+
+### 3. R4B Routing In `plan-executor`
+
+- Add `r4b` parsing to the rake version resolver and test that omitted and
+ unknown versions fail fast.
+- Replace scattered version conditionals with a central namespace resolver where practical.
+- Add explicit R4B namespace and resource resolution in `BaseTest`, `BaseSuite`, OperationOutcome parsing, capability statement handling, fixture validation, resource category lookup, and resource generation helpers.
+- Initialize R4B base resources with the active client without affecting R4, STU3, or DSTU2 resources.
+- Add `lib/FHIR_structure_r4b.json`, generated from the same pinned R4B definitions used by the models.
+- Extend structure tests to compare R4B structure metadata against `FHIR::R4B::RESOURCES`, and fix existing structure-root tests so they make assertions.
+- Fix version-specific fixture override lookup before adding `*.r4b.xml` or `*.r4b.json` fixtures.
+- Version-specific fixture overrides are stored beside the base fixture as
+ `.r4b.xml` or `.r4b.json`. Lookup falls back to the base fixture
+ only when no version-specific file exists. Reuse of a base fixture still
+ requires validation before a suite can declare R4B compatibility.
+- Update README and shell usage documentation to list `r4b`.
+
+## Pitfalls To Avoid
+
+- Do not let `r4b` fall into top-level `FHIR::Patient` or `FHIR::Bundle` R4 classes by default.
+- Do not treat passing R4 smoke tests as proof of R4B support.
+- Do not reuse R4 fixtures for R4B unless validation proves they are compatible.
+- Do not enable STU3 TestScripts for R4B unless separate R4B TestScripts are added.
+- Watch for model/client dependency gaps where R4B model classes exist but parsing, capability statements, or client version activation do not.
+- Do not let metadata listing advertise R4B support that execution would reject, or vice versa.
+- Do not treat a normative resource as automatically compatible at the suite level; test semantics, search parameters, fixtures, and assertions still require an explicit audit.
+- Do not allow generated R4B resources to contain top-level R4 complex types or contained resources.
+- Do not publish the harness against a client or model dependency that is only available through an unrecorded local checkout.
+
+## Validation Milestones
+
+### Model Validation
+
+- R4B JSON and XML examples parse into `FHIR::R4B::*` and round-trip successfully.
+- Bundle entries and contained resources remain entirely within `FHIR::R4B`.
+- R4B resource generation produces valid resources without top-level R4 model instances.
+- At least one R4B-only field or changed structure is accepted by R4B validation and rejected by R4 validation, proving that R4B is not an alias.
+- R4 behavior and its existing top-level namespace remain unchanged.
+
+### Client Validation
+
+- Explicit `use_r4`, `use_r4b`, `use_stu3`, and `use_dstu2` select the expected model namespace.
+- CapabilityStatement detection distinguishes FHIR `4.0.x` from `4.3.x` and rejects unsupported versions.
+- Read, search, create/update, Bundle, OperationOutcome, transaction, and format handling parse responses through the selected namespace.
+
+### Harness Validation
+
+- `bundle exec rake crucible:list_all[r4b]` lists only suites explicitly annotated for R4B.
+- Resource-based suites instantiate R4B classes, not R4 classes, and only enumerate resources present in `FHIR::R4B::RESOURCES`.
+- Listing and executing suites apply identical version eligibility rules.
+- An omitted or unknown CLI version exits with a clear error. R4 must be
+ selected explicitly with `r4`.
+- `FHIR_structure_r4b.json` passes resource-list consistency and duplicate-name checks.
+- Version-specific fixture override selection has focused unit coverage.
+- `FormatTest` passes all 22 cases against a Spark endpoint whose
+ CapabilityStatement reports FHIR `4.3.0` and both `xml` and `json`. The audit
+ used `sparkfhir/spark:r4b-latest` image
+ `sha256:720309c969f8562f418948197b794cc01cc07e5be2d7a82873562a8714719e82`
+ and `sparkfhir/mongo:r4b-latest` image
+ `sha256:10a44ee9fa2c6a42325656b1758b3fb14be00dfe84a6d999e1b9b3d695fc29d5`.
+ This covers Patient read, search Bundle, JSON, XML, and format negotiation.
+- Existing R4, STU3, and DSTU2 tests remain unchanged in behavior and pass their regression matrix.
+
+## R4B Full-Suite Endpoint Audit (2026-07-17)
+
+### Scope And Execution
+
+- This was a diagnostic run against the local Spark R4B endpoint at
+ `http://localhost:18080/fhir`. It used the same Spark and Mongo image digests
+ recorded for the FormatTest audit above.
+- The run used temporary `path:` dependencies for the local `../fhir_models`,
+ `../fhir_client`, `../fhir_stu3_models`, and `../fhir_dstu2_models`
+ repositories so the unpublished R4B model and client changes were loaded.
+- Only suites whose existing `supported_versions` declaration contained `:r4`
+ were temporarily given `:r4b`. STU3-only, DSTU2-only, TestScript, and
+ explicitly unsupported suites were not enabled. These temporary annotations
+ were made in an isolated copy. The later 2026-07-23 verification below
+ records the permanent annotations after strict version routing was added.
+- The normal suite registry exposed 12 eligible suites. Eligibility was checked
+ with:
+
+ ```sh
+ bundle exec rake "crucible:list_suites[r4b]"
+ ```
+
+- Each suite was then run separately through the documented Rake entry point:
+
+ ```sh
+ bundle exec rake "crucible:execute[http://localhost:18080/fhir,r4b,SUITE_NAME,,stdout]"
+ ```
+
+ The empty resource argument caused `ResourceTest` and `SearchTest` to exercise
+ every R4B resource rather than a single named resource. Suites were run
+ sequentially because they create, update, and delete shared endpoint data.
+- The test process must be allowed to connect to the local endpoint. A sandboxed
+ attempt produced `Operation not permitted` TCP errors and invalid all-skip or
+ all-error results; those results were discarded. The Rake task also expects
+ the local `logs/` directory to exist.
+
+### Results
+
+| Suite | Pass | Fail | Error | TODO skip | Exit |
+| --- | ---: | ---: | ---: | ---: | ---: |
+| `SprinklerSearchTest` | 36 | 0 | 0 | 2 | 0 |
+| `ConsentSearchByPatientReferenceTest` | 1 | 0 | 0 | 0 | 0 |
+| `ElementsSearchParameterTest` | 1 | 0 | 0 | 1 | 0 |
+| `UnknownSearchParameterTest` | 12 | 0 | 0 | 0 | 0 |
+| `ReadTest` | 5 | 0 | 0 | 0 | 0 |
+| `ResourceTest` | 2075 | 10 | 0 | 417 | 1 |
+| `FhirPathPatchTest` | 4 | 0 | 0 | 2 | 0 |
+| `FormatTest` | 22 | 0 | 0 | 0 | 0 |
+| `TransactionAndBatchTest` | 2 | 6 | 0 | 5 | 1 |
+| `SearchTest` | 973 | 0 | 0 | 0 | 0 |
+| `HistoryTest` | 10 | 0 | 0 | 0 | 0 |
+| `RobustSearchTest` | 0 | 0 | 0 | 1 | 0 |
+| **Total** | **3141** | **16** | **0** | **428** | **2 suites failed** |
+
+All 428 skips were existing `TODO` skips. They do not cause the Rake task to
+exit non-zero. The 16 failures are concentrated in the two suites shown above;
+they are not 16 independent compatibility defects.
+
+### Failure Areas
+
+#### 1. R4B Condition Status Conversion
+
+- `TransactionAndBatchTest` fails first in `XFER0`. The generated transaction
+ serializes `Condition.verificationStatus` as the primitive string
+ `"confirmed"`, while R4B requires a `CodeableConcept`.
+- Spark returns HTTP 400 with an OperationOutcome reporting that it encountered
+ a JSON primitive where a non-primitive `verificationStatus` object was
+ required. Five later transaction assertions then fail because they depend on
+ `XFER0` having created the patient record.
+- `ResourceGenerator.fix_condition` currently converts R4 status strings only
+ when `resource.is_a?(FHIR::Condition)`. A `FHIR::R4B::Condition` does not
+ satisfy that check, so the existing R4 compatibility correction is skipped.
+- The correction must become namespace-aware and cover both
+ `clinicalStatus` and `verificationStatus` without making R4B inherit from or
+ fall back to the R4 model class. Add focused serialization coverage before
+ rerunning `TransactionAndBatchTest`.
+
+#### 2. Recursive PackagedProductDefinition Generation
+
+- `ResourceTest` deterministically generates invalid deeply nested
+ `PackagedProductDefinition` resources. At the generator recursion boundary,
+ `package.package[].package[].containedItem[].item` is omitted even though its
+ minimum cardinality is one.
+- Spark rejects these resources with HTTP 400. The initial create failures then
+ cause conditional create, conditional update, and history assertions to fail
+ or operate on incomplete setup state.
+- A targeted documented run reproduces the problem:
+
+ ```sh
+ bundle exec rake "crucible:execute[http://localhost:18080/fhir,r4b,ResourceTest,PackagedProductDefinition,stdout]"
+ ```
+
+- Fixing this requires a finite minimal representation for the recursive
+ package structure. The recursion guard must still prevent infinite trees, but
+ it cannot terminate by omitting a required child. Add a generator test that
+ validates the generated JSON or XML against the R4B model/schema.
+
+#### 3. Abstract Questionnaire Item Code Generation
+
+- The generated R4B metadata for `Questionnaire.item.type` includes the
+ abstract code `question` in `valid_codes`. `ResourceGenerator` samples from
+ that list and may emit `type: "question"` in one or more nested items.
+- Spark rejects that value because it is not a selectable
+ `QuestionnaireItemType`. The exact number of failed ResourceTest assertions
+ varies with random generation; a targeted rerun reproduced failures in
+ create and update operations.
+- Reproduce with:
+
+ ```sh
+ bundle exec rake "crucible:execute[http://localhost:18080/fhir,r4b,ResourceTest,Questionnaire,stdout]"
+ ```
+
+- Preserve the complete terminology definitions needed at runtime, but prevent
+ abstract or non-selectable codes from being chosen for generated resource
+ instances. Add deterministic coverage proving that generated Questionnaire
+ items use only concrete item types.
+
+### Follow-Up Sequence
+
+1. Add focused failing tests for R4B Condition status conversion,
+ PackagedProductDefinition recursion, and Questionnaire item-type selection.
+2. Make the resource generator namespace-aware where it currently dispatches
+ only on top-level R4 classes. Review the rest of `apply_invariants!` for the
+ same pattern.
+3. Fix and rerun the three targeted commands above, including
+ `TransactionAndBatchTest` through `crucible:execute`.
+4. Repeat the complete 12-suite R4B endpoint run and retain per-suite output and
+ shell exit status.
+5. Run the existing R4 unit and endpoint regression suites to detect shared
+ generator regressions.
+
+### Existing Endpoint Regression Baselines
+
+The following existing endpoint results were provided on 2026-07-17. They were
+not rerun as part of the R4B audit, but should be retained as the comparison
+baseline for later cross-version regression runs.
+
+| Version | Pass | Fail | Error | Skip |
+| --- | ---: | ---: | ---: | ---: |
+| STU3 | 2876 | 0 | 0 | 413 |
+| R4 | 3267 | 0 | 0 | 443 |
+
+Future runs should compare both the totals and the individual skipped tests.
+The totals alone do not establish whether a changed skip is expected.
+
+## Strict Versioning Docker Verification (2026-07-23)
+
+- The Docker image was built with the repository `Dockerfile` and a disposable
+ build context containing the current local `fhir_client`, `fhir_models`,
+ `fhir_stu3_models`, and `fhir_dstu2_models` working trees as `path:`
+ dependencies. The resulting image was
+ `incendi/plan_executor:strict-r4b`, image ID
+ `sha256:b5ae435552fd1305d072c021d1b140db1461daa2b23d2cd1183855ec96bfa975`.
+- The plan-executor unit suite passed `1218` tests and `3680` assertions with no
+ failures or errors inside that image.
+- Focused `fhir_client` coverage for required versions, `:auto`, R4B routing,
+ and external references passed `79` tests and `186` assertions with no
+ failures or errors.
+- The complete modified `fhir_client` suite reported `114` tests, `269`
+ assertions, and the same five errors reproduced by the committed baseline in
+ the identical container. Four are caused by invalid JSON escapes in the
+ existing `fhir_api_validation.json`; one is existing shared model-client
+ state in `test_class_partial_update`. The strict-versioning change introduced
+ no additional full-suite failures.
+- The CI-style Compose run used
+ `sparkfhir/spark:r4b-latest` image ID
+ `sha256:d5139dcba0a3e17aac71b36d31271326111248ad4af1bc16b095d423f2b7d2d8`
+ and `sparkfhir/mongo:r4b-latest` image ID
+ `sha256:9c8e741da8cbce3b5e10e845717c368f41a2e27311912541ed2192110d4d7741`.
+ The initial
+ `./execute_all.sh http://spark:8080/fhir r4b html|json|stdout` run included
+ only `FormatTest` and passed all `22` cases. The expanded run is recorded
+ below.
+
+## R4B Enabled-Suite Verification (2026-07-23)
+
+- Every Ruby suite that declares R4 support now also explicitly declares R4B
+ support. `crucible:list_suites[r4b]` lists 12 suites:
+ `ReadTest`, `ResourceTest`, `FhirPathPatchTest`, `FormatTest`,
+ `TransactionAndBatchTest`, `SearchTest`, `HistoryTest`, `RobustSearchTest`,
+ `SprinklerSearchTest`, `ConsentSearchByPatientReferenceTest`,
+ `ElementsSearchParameterTest`, and `UnknownSearchParameterTest`.
+- `FhirPathPatchTest` now obtains `MedicationRequest` through its selected
+ version namespace instead of using the top-level R4 model class. An audit of
+ the other 11 suites found no unversioned resource-model constants.
+- A unit invariant requires the set of R4 suites and the set of R4B suites to
+ remain identical. The Docker unit run passed `1218` tests and `3680`
+ assertions with no failures or errors.
+- The CI-style endpoint command was:
+
+ ```sh
+ docker compose run --rm --no-deps plan_executor \
+ ./execute_all.sh http://spark:8080/fhir r4b 'html|json|stdout'
+ ```
+
+- The disposable plan-executor image was
+ `incendi/plan_executor:all-r4b`, image ID
+ `sha256:7891bc0c6347a99b54d5b5382b9081d7c8b422aee6bd3d85ca93e73c4cdd3221`.
+ The Spark and Mongo image IDs were
+ `sha256:d5139dcba0a3e17aac71b36d31271326111248ad4af1bc16b095d423f2b7d2d8`
+ and
+ `sha256:9c8e741da8cbce3b5e10e845717c368f41a2e27311912541ed2192110d4d7741`.
+- The endpoint run completed 3,585 tests in 234 seconds:
+
+ | Pass | Fail | Error | TODO skip |
+ | ---: | ---: | ---: | ---: |
+ | 3149 | 8 | 0 | 428 |
+
+- All eight failures in this run were in
+ `ResourceTest_PackagedProductDefinition`. Three create or update requests
+ were rejected because generated nested package entries omitted required
+ `containedItem.item`; five conditional and history assertions then failed
+ because those resources were not created.
+- All 428 skips remain existing `TODO` skips. FHIR TestScript artifacts remain
+ explicitly STU3-only and were not part of this R4B run.
+
+### PackagedProductDefinition Generator Fix
+
+- The root cause was a recursion-boundary `CodeableReference` object with
+ neither `concept` nor `reference`. Although the required object existed in
+ memory, it serialized as an empty object and the effective
+ `containedItem.item` element was omitted.
+- `ResourceGenerator` now gives an otherwise empty generated
+ `CodeableReference` a text-only `CodeableConcept` from the selected FHIR
+ namespace. Existing concept or reference values are preserved. This is a
+ datatype invariant rather than a PackagedProductDefinition-specific
+ traversal.
+- Focused unit coverage checks empty and populated R4B CodeableReference
+ behavior, every recursively generated contained item, and R4B XML schema
+ validation. The Docker unit suite passed `1221` tests and `3688` assertions
+ with no failures or errors.
+- A fresh focused endpoint run of
+ `ResourceTest_PackagedProductDefinition` completed with `15` passes, no
+ failures or errors, and the existing `3` TODO skips.
+- A subsequent fresh 12-suite run confirmed the same PackagedProductDefinition
+ result. The aggregate result was `3151` passes, `6` failures, no errors, and
+ `428` TODO skips. All six failures were the separately documented
+ nondeterministic Questionnaire `type: question` defect; none belonged to
+ PackagedProductDefinition.
+- Verification used `incendi/plan_executor:packaged-product-fix`, image ID
+ `sha256:b8a9ee41b11496a99e38a6dc93ba061d720075477c31d9ea5deafcf6f5900c62`,
+ with the same R4B Spark and Mongo image IDs recorded above.
+
+### Questionnaire Selectable-Code Fix
+
+- Generated model metadata continues to contain the complete required
+ `QuestionnaireItemType` code set, including the abstract `question` grouping
+ code. The terminology artifacts and generated models were not rewritten.
+- `ResourceGenerator` now derives a cached selectable-code set from the
+ namespace's original ValueSet expansion. Entries marked `abstract` or
+ `inactive` are excluded from generated instances, while concrete descendants
+ of abstract grouping entries remain selectable.
+- The same filtering is used for primitive `code`, `Coding`, and
+ `CodeableConcept` generation. If a binding has no matching expansion or
+ filtering would remove every generated code, the existing generated metadata
+ remains the fallback.
+- Focused tests prove that the complete R4B Questionnaire metadata still
+ includes `question`, the selectable set excludes it, and every recursively
+ generated Questionnaire item uses a selectable type. The Docker unit suite
+ passed `1224` tests and `3694` assertions with no failures or errors.
+- Three initial consecutive `ResourceTest_Questionnaire` endpoint runs each
+ completed with `15` passes and the existing `3` TODO skips. After correcting
+ the cache scope to cache expansion data rather than field-specific
+ intersections, the final image produced the same focused result and no
+ generated Questionnaire payload contained `type: question`.
+- A final fresh 12-suite R4B run completed in 236 seconds with `3157` passes,
+ no failures or errors, and `428` existing TODO skips. The command exited
+ successfully, and PackagedProductDefinition remained clean in the same run.
+- Verification used `incendi/plan_executor:questionnaire-fix`, image ID
+ `sha256:f6dda44f11bcf8eafac55695d4e9f066f377473b3310963e2b3d94fa83ca7680`,
+ with the same R4B Spark and Mongo image IDs recorded above.
+
+### R4 Regression Verification
+
+- The final Questionnaire implementation was run against the fresh
+ `sparkfhir/spark:r4-latest` and `sparkfhir/mongo:r4-latest` images, with image
+ IDs
+ `sha256:411d6ea0d92c8001359eec3d749c643a28b207f3ac6f0f3362f1d39081348bf7`
+ and
+ `sha256:cf64b34e58f6f88f5350cef5066456f69585e4ec4716bcdb56579e3153950e4c`.
+ The plan-executor image was the same final image recorded above.
+- The R4 Spark image contains a local Kestrel HTTPS endpoint configuration but
+ no server certificate. Its first startup therefore entered a restart loop,
+ and that infrastructure-only run was discarded. The clean retry used the
+ disposable environment override
+ `Kestrel__Endpoints__Https__Url=http://+:8080`; neither the image nor
+ repository configuration was changed.
+- The fresh full R4 run completed in 145 seconds with `3267` passes, no failures
+ or errors, and `443` TODO skips. The command exited successfully and exactly
+ matched the recorded R4 aggregate baseline.
+- `ResourceTest_Questionnaire` completed with `15` passes, no failures or
+ errors, and the existing `3` TODO skips. No generated Questionnaire payload
+ contained `type: question`.
+
+### Merged Dependency Verification
+
+- `fhir_models` resolves from
+ `https://github.com/incendilabs/fhir_models.git` master revision
+ `a143d2e21d0253b33fdaeb17e2d152ad656c9a3e`.
+- `fhir_client` resolves from
+ `https://github.com/incendilabs/fhir_client.git` master revision
+ `79026641f9b2ac7cf30bc27a3528e505d34c67e8`.
+- A repository Docker build using only the merged Git dependencies produced
+ `incendi/plan_executor:master-r4b-deps`, image ID
+ `sha256:927c11680443ee857d039ec71eea63cfa8b6c28da5a96d494a1c5b9c9f974345`.
+ The image loaded `fhir_client` 5.0.0, `fhir_models` 4.1.0, and
+ `FHIR::R4B::Patient`.
+- The Docker unit suite passed `1224` tests and `3694` assertions with no
+ failures or errors.
+
+### Deferred STU3 TestScript Regression
+
+- Run the 71 FHIR TestScript artifacts against a STU3 endpoint as a separate
+ regression exercise. They remain explicitly STU3-only and are not part of
+ either the R4 or R4B suite runs.
+- The dedicated `crucible:execute_all_testscripts` and
+ `crucible:testreport` tasks now require a FHIR version and construct the
+ client with that version. They still bypass the normal suite
+ `supported_versions` filter, so the deferred regression must invoke them with
+ `stu3` explicitly and verify the endpoint version.
+- The eventual regression command must explicitly select `:stu3`, retain
+ per-TestScript output and shell exit status, and use a CapabilityStatement to
+ confirm that the target endpoint reports STU3 before execution.
+
+## Recommended Implementation Order
+
+1. Complete: reconcile the `fhir_models` baseline and dependency strategy.
+2. Complete: refactor `fhir_models` generation and runtime for explicit `FHIR::R4B` support.
+3. Complete: generate and validate the R4B model set.
+4. Complete: add R4B routing, parsing, capability handling, and detection to `fhir_client`.
+5. Complete: add the central version registry and fail-fast version resolution to `plan-executor`.
+6. Complete: all 12 R4-capable Ruby suites explicitly declare R4B support and
+ have been run against the R4B endpoint.
+7. In progress: complete the STU3 and DSTU2 endpoint regression matrix. Merged
+ dependency resolution and fresh R4 and R4B endpoint runs are complete.
+8. Complete: remove implicit R4 defaults from `fhir_client` and
+ `plan-executor`, make generator namespaces explicit, verify the breaking
+ change in Docker, and commit it atomically.
diff --git a/README.md b/README.md
index 803e4105..6bb55fd2 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,14 @@
# Plan Executor [](https://travis-ci.org/fhir-crucible/plan_executor)
-Plan Executor runs test suites against a FHIR server. It supports `DSTU2`, `STU3` and `R4` versions of FHIR.
+Plan Executor runs test suites against a FHIR server. The harness recognizes
+`DSTU2`, `STU3`, `R4`, and `R4B` versions of FHIR. Each suite declares its
+supported versions explicitly; recognizing a version does not make every suite
+compatible with it.
+Commands that execute suites require this version argument; omission is an
+error and does not select R4 implicitly.
Tests can either be written in [Ruby](https://github.com/fhir-crucible/plan_executor#adding-a-new-test-suite),
or using the [TestScript Resource](https://github.com/fhir-crucible/plan_executor/wiki/Using-Plan-Executor-with-TestScripts#testscript).
+TestScript execution remains STU3-only.
## Getting Started
@@ -13,10 +19,13 @@ $ bundle exec rake -T
## Listing Test Suites
-List all the available Test Suites, excluding supported `TestScripts`. Pass the version, which can currently be `dstu2`, `stu3` or `r4`.
+List all available Test Suites, excluding supported `TestScripts`. Pass the
+version, which can be `dstu2`, `stu3`, `r4`, or `r4b`. Only suites explicitly
+annotated for the selected version are listed.
```
$ bundle exec rake crucible:list_suites[dstu2]
+$ bundle exec rake crucible:list_suites[r4b]
```
## Executing a Test Suite
@@ -24,7 +33,7 @@ $ bundle exec rake crucible:list_suites[dstu2]
Crucible tests can be executed by suite from the command-line by calling the `crucible-execute` rake task with the following parameters:
* `url` the FHIR endpoint
-* `version` the FHIR version (sequence). Currently `dstu2`, `stu3` and `r4` are supported.
+* `version` the FHIR version (sequence): `dstu2`, `stu3`, `r4`, or `r4b`.
* `test` the name of the test suite (see `crucible:list_suites`)
* `resource` (optional) limit the `test` (applicable to "ResourceTest" or "SearchTest" suites) to a given resource (e.g. "Patient")
@@ -66,12 +75,13 @@ module Crucible
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:r4]
@category = {id: 'connectathon', title: 'Connectathon'}
end
def setup
# create any fixtures you need here
- @patient = ResourceGenerator.generate(FHIR::Patient,3)
+ @patient = ResourceGenerator.generate(get_resource(:Patient),3)
reply = @client.create(@patient)
@id = reply.id
@body = reply.body
@@ -79,7 +89,7 @@ module Crucible
def teardown
# perform any clean up here
- @client.destroy(FHIR::Patient, @id)
+ @client.destroy(get_resource(:Patient), @id)
end
# test 'KEY', 'DESCRIPTION'
@@ -91,7 +101,7 @@ module Crucible
}
assert(@id, 'Setup was unable to create a patient.',@body)
- reply = @client.read(FHIR::Patient, @id)
+ reply = @client.read(get_resource(:Patient), @id)
assert_response_ok(reply)
assert_equal @id, reply.id, 'Server returned wrong patient.'
warning { assert_valid_resource_content_type_present(reply) }
@@ -108,6 +118,8 @@ Every Test Suite needs to override the following methods:
* `description` The description that is displayed within the Crucible web app
* `initialize` Use the example above. Change the `@category` -- the `id` and `title`
determine where the test suite is categorized within the Crucible web app
+and set `@supported_versions` explicitly. A suite is not eligible for any FHIR
+version until that annotation is present.
* `setup` (optional) Use this method to create fixtures and perform any required
assertions prior to execution of individual `test` blocks.
* `test` These blocks are the individual tests within the suites. Each block should start with a `metadata` section so Crucible knows how to tie the success or failures to portions of the FHIR specification (displayed in the web app with a starburst). See `lib/FHIR_structure.json` for the values associated with the `name` keys that you can link to.
diff --git a/docker-compose-r4b.yml b/docker-compose-r4b.yml
new file mode 100644
index 00000000..5e4510bf
--- /dev/null
+++ b/docker-compose-r4b.yml
@@ -0,0 +1,5 @@
+services:
+ spark:
+ image: sparkfhir/spark:r4b-latest
+ mongodb:
+ image: sparkfhir/mongo:r4b-latest
diff --git a/lib/FHIR_structure_r4b.json b/lib/FHIR_structure_r4b.json
new file mode 100644
index 00000000..242e1ac7
--- /dev/null
+++ b/lib/FHIR_structure_r4b.json
@@ -0,0 +1,936 @@
+{
+ "name": "FHIR",
+ "children": [
+ {
+ "name": "OPERATIONS",
+ "children": [
+ {
+ "name": "RESTful API",
+ "children": [
+ {
+ "name": "Instance Level Interactions",
+ "children": [
+ {
+ "name": "read"
+ },
+ {
+ "name": "vread"
+ },
+ {
+ "name": "update"
+ },
+ {
+ "name": "conditional-update"
+ },
+ {
+ "name": "delete"
+ },
+ {
+ "name": "history"
+ },
+ {
+ "name": "patch"
+ }
+ ]
+ },
+ {
+ "name": "Type Level Interactions",
+ "children": [
+ {
+ "name": "create"
+ },
+ {
+ "name": "conditional-create"
+ },
+ {
+ "name": "search"
+ },
+ {
+ "name": "history-type"
+ }
+ ]
+ },
+ {
+ "name": "Whole System Interactions",
+ "children": [
+ {
+ "name": "capabilities-system"
+ },
+ {
+ "name": "transaction-system"
+ },
+ {
+ "name": "history-system"
+ },
+ {
+ "name": "search-system"
+ },
+ {
+ "name": "batch-system"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Extended Operations",
+ "children": [
+ {
+ "name": "$validate"
+ },
+ {
+ "name": "$meta"
+ },
+ {
+ "name": "$meta-add"
+ },
+ {
+ "name": "$meta-delete"
+ },
+ {
+ "name": "$convert"
+ },
+ {
+ "name": "$graph-ql"
+ },
+ {
+ "name": "$graph"
+ },
+ {
+ "name": "$apply"
+ },
+ {
+ "name": "$data-requirements"
+ },
+ {
+ "name": "$subset"
+ },
+ {
+ "name": "$implements"
+ },
+ {
+ "name": "$conforms"
+ },
+ {
+ "name": "$versions"
+ },
+ {
+ "name": "$submit"
+ },
+ {
+ "name": "$submit-data"
+ },
+ {
+ "name": "$collect-data"
+ },
+ {
+ "name": "$care-gaps"
+ },
+ {
+ "name": "$preferred-id"
+ },
+ {
+ "name": "$stats"
+ },
+ {
+ "name": "$lastn"
+ },
+ {
+ "name": "$transform"
+ },
+ {
+ "name": "$snapshot"
+ },
+ {
+ "name": "$subsumes"
+ },
+ {
+ "name": "$find-matches"
+ },
+ {
+ "name": "$document"
+ },
+ {
+ "name": "$translate"
+ },
+ {
+ "name": "$closure"
+ },
+ {
+ "name": "$everything"
+ },
+ {
+ "name": "$find"
+ },
+ {
+ "name": "$process-message"
+ },
+ {
+ "name": "$populate"
+ },
+ {
+ "name": "$questionnaire"
+ },
+ {
+ "name": "$expand"
+ },
+ {
+ "name": "$lookup"
+ },
+ {
+ "name": "$validate-code"
+ },
+ {
+ "name": "$guidance"
+ },
+ {
+ "name": "$guidance-requirements"
+ },
+ {
+ "name": "$match"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "RESOURCES",
+ "children": [
+ {
+ "name": "Foundation",
+ "children": [
+ {
+ "name": "Conformance",
+ "children": [
+ {
+ "name": "capability statement"
+ },
+ {
+ "name": "compartment definition"
+ },
+ {
+ "name": "example scenario"
+ },
+ {
+ "name": "graph definition"
+ },
+ {
+ "name": "implementation guide"
+ },
+ {
+ "name": "message definition"
+ },
+ {
+ "name": "operation definition"
+ },
+ {
+ "name": "search parameter"
+ },
+ {
+ "name": "structure definition"
+ },
+ {
+ "name": "structure map"
+ }
+ ]
+ },
+ {
+ "name": "Terminology",
+ "children": [
+ {
+ "name": "code system"
+ },
+ {
+ "name": "concept map"
+ },
+ {
+ "name": "naming system"
+ },
+ {
+ "name": "terminology capabilities"
+ },
+ {
+ "name": "value set"
+ }
+ ]
+ },
+ {
+ "name": "Security",
+ "aka": [
+ "Security & Privacy"
+ ],
+ "children": [
+ {
+ "name": "audit event"
+ },
+ {
+ "name": "consent"
+ },
+ {
+ "name": "provenance"
+ }
+ ]
+ },
+ {
+ "name": "Documents",
+ "aka": [
+ "Documents & Questionnaires"
+ ],
+ "children": [
+ {
+ "name": "catalog entry"
+ },
+ {
+ "name": "composition"
+ },
+ {
+ "name": "document manifest"
+ },
+ {
+ "name": "document reference"
+ }
+ ]
+ },
+ {
+ "name": "Other",
+ "aka": [
+ "Exchange",
+ "Structure",
+ "Structures"
+ ],
+ "children": [
+ {
+ "name": "basic"
+ },
+ {
+ "name": "binary"
+ },
+ {
+ "name": "bundle"
+ },
+ {
+ "name": "linkage"
+ },
+ {
+ "name": "message header"
+ },
+ {
+ "name": "operation outcome"
+ },
+ {
+ "name": "parameters"
+ },
+ {
+ "name": "subscription"
+ },
+ {
+ "name": "subscription status"
+ },
+ {
+ "name": "subscription topic"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Base",
+ "aka": [
+ "Administration",
+ "Administrative",
+ "Identification"
+ ],
+ "children": [
+ {
+ "name": "Individuals",
+ "children": [
+ {
+ "name": "group"
+ },
+ {
+ "name": "patient"
+ },
+ {
+ "name": "person"
+ },
+ {
+ "name": "practitioner"
+ },
+ {
+ "name": "practitioner role"
+ },
+ {
+ "name": "related person"
+ }
+ ]
+ },
+ {
+ "name": "Entities",
+ "children": [
+ {
+ "name": "biologically derived product"
+ },
+ {
+ "name": "device"
+ },
+ {
+ "name": "device metric"
+ },
+ {
+ "name": "endpoint"
+ },
+ {
+ "name": "healthcare service"
+ },
+ {
+ "name": "location"
+ },
+ {
+ "name": "nutrition product"
+ },
+ {
+ "name": "organization"
+ },
+ {
+ "name": "organization affiliation"
+ },
+ {
+ "name": "substance"
+ }
+ ]
+ },
+ {
+ "name": "Workflow",
+ "aka": [
+ "Scheduling",
+ "Events",
+ "Order Management"
+ ],
+ "children": [
+ {
+ "name": "appointment"
+ },
+ {
+ "name": "appointment response"
+ },
+ {
+ "name": "schedule"
+ },
+ {
+ "name": "slot"
+ },
+ {
+ "name": "task"
+ },
+ {
+ "name": "verification result"
+ }
+ ]
+ },
+ {
+ "name": "Management",
+ "aka": [
+ "Patient Management"
+ ],
+ "children": [
+ {
+ "name": "encounter"
+ },
+ {
+ "name": "episode of care"
+ },
+ {
+ "name": "flag"
+ },
+ {
+ "name": "library"
+ },
+ {
+ "name": "list"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Clinical",
+ "children": [
+ {
+ "name": "Summary",
+ "aka": [
+ "General Clinical"
+ ],
+ "children": [
+ {
+ "name": "adverse event"
+ },
+ {
+ "name": "allergy intolerance"
+ },
+ {
+ "name": "clinical impression"
+ },
+ {
+ "name": "condition"
+ },
+ {
+ "name": "detected issue"
+ },
+ {
+ "name": "family member history"
+ },
+ {
+ "name": "procedure"
+ }
+ ]
+ },
+ {
+ "name": "Diagnostics",
+ "children": [
+ {
+ "name": "body structure"
+ },
+ {
+ "name": "diagnostic report"
+ },
+ {
+ "name": "imaging study"
+ },
+ {
+ "name": "media"
+ },
+ {
+ "name": "molecular sequence"
+ },
+ {
+ "name": "observation"
+ },
+ {
+ "name": "questionnaire response"
+ },
+ {
+ "name": "specimen"
+ }
+ ]
+ },
+ {
+ "name": "Medications",
+ "aka": [
+ "Medication & Immunization"
+ ],
+ "children": [
+ {
+ "name": "immunization"
+ },
+ {
+ "name": "immunization evaluation"
+ },
+ {
+ "name": "immunization recommendation"
+ },
+ {
+ "name": "medication"
+ },
+ {
+ "name": "medication administration"
+ },
+ {
+ "name": "medication dispense"
+ },
+ {
+ "name": "medication knowledge"
+ },
+ {
+ "name": "medication request"
+ },
+ {
+ "name": "medication statement"
+ }
+ ]
+ },
+ {
+ "name": "Care Provision",
+ "children": [
+ {
+ "name": "care plan"
+ },
+ {
+ "name": "care team"
+ },
+ {
+ "name": "goal"
+ },
+ {
+ "name": "nutrition order"
+ },
+ {
+ "name": "request group"
+ },
+ {
+ "name": "risk assessment"
+ },
+ {
+ "name": "service request"
+ },
+ {
+ "name": "vision prescription"
+ }
+ ]
+ },
+ {
+ "name": "Request & Response",
+ "children": [
+ {
+ "name": "communication"
+ },
+ {
+ "name": "communication request"
+ },
+ {
+ "name": "device request"
+ },
+ {
+ "name": "device use statement"
+ },
+ {
+ "name": "guidance response"
+ },
+ {
+ "name": "supply delivery"
+ },
+ {
+ "name": "supply request"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Financial",
+ "children": [
+ {
+ "name": "Support",
+ "children": [
+ {
+ "name": "coverage"
+ },
+ {
+ "name": "coverage eligibility request"
+ },
+ {
+ "name": "coverage eligibility response"
+ },
+ {
+ "name": "enrollment request"
+ },
+ {
+ "name": "enrollment response"
+ }
+ ]
+ },
+ {
+ "name": "Billing",
+ "children": [
+ {
+ "name": "claim"
+ },
+ {
+ "name": "claim response"
+ },
+ {
+ "name": "invoice"
+ }
+ ]
+ },
+ {
+ "name": "Payment",
+ "children": [
+ {
+ "name": "payment notice"
+ },
+ {
+ "name": "payment reconciliation"
+ }
+ ]
+ },
+ {
+ "name": "General",
+ "aka": [
+ "Other"
+ ],
+ "children": [
+ {
+ "name": "account"
+ },
+ {
+ "name": "charge item"
+ },
+ {
+ "name": "charge item definition"
+ },
+ {
+ "name": "contract"
+ },
+ {
+ "name": "explanation of benefit"
+ },
+ {
+ "name": "insurance plan"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Specialized",
+ "children": [
+ {
+ "name": "Public Health & Research",
+ "aka": [
+ "Research"
+ ],
+ "children": [
+ {
+ "name": "research study"
+ },
+ {
+ "name": "research subject"
+ }
+ ]
+ },
+ {
+ "name": "Definitional Artifacts",
+ "children": [
+ {
+ "name": "activity definition"
+ },
+ {
+ "name": "device definition"
+ },
+ {
+ "name": "event definition"
+ },
+ {
+ "name": "observation definition"
+ },
+ {
+ "name": "plan definition"
+ },
+ {
+ "name": "questionnaire"
+ },
+ {
+ "name": "specimen definition"
+ }
+ ]
+ },
+ {
+ "name": "Evidence-Based Medicine",
+ "children": [
+ {
+ "name": "citation"
+ },
+ {
+ "name": "evidence"
+ },
+ {
+ "name": "evidence report"
+ },
+ {
+ "name": "evidence variable"
+ },
+ {
+ "name": "research definition"
+ },
+ {
+ "name": "research element definition"
+ }
+ ]
+ },
+ {
+ "name": "Quality Reporting & Testing",
+ "aka": [
+ "Clinical Reasoning",
+ "Quality Reporting"
+ ],
+ "children": [
+ {
+ "name": "measure"
+ },
+ {
+ "name": "measure report"
+ },
+ {
+ "name": "test report"
+ },
+ {
+ "name": "test script"
+ }
+ ]
+ },
+ {
+ "name": "Medication Definition",
+ "children": [
+ {
+ "name": "administrable product definition"
+ },
+ {
+ "name": "clinical use definition"
+ },
+ {
+ "name": "ingredient"
+ },
+ {
+ "name": "manufactured item definition"
+ },
+ {
+ "name": "medicinal product definition"
+ },
+ {
+ "name": "packaged product definition"
+ },
+ {
+ "name": "regulated authorization"
+ },
+ {
+ "name": "substance definition"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "FORMAT",
+ "children": [
+ {
+ "name": "XML"
+ },
+ {
+ "name": "JSON"
+ }
+ ]
+ },
+ {
+ "name": "SECURITY",
+ "children": [
+ {
+ "name": "General Security",
+ "children": [
+ {
+ "name": "Authorization/Access Control"
+ },
+ {
+ "name": "OAuth2"
+ },
+ {
+ "name": "Audit Logging"
+ },
+ {
+ "name": "Digital Signatures"
+ }
+ ]
+ },
+ {
+ "name": "Security Labels",
+ "children": [
+ {
+ "name": "Confidentiality Codes"
+ },
+ {
+ "name": "Celebrity / VIP"
+ },
+ {
+ "name": "Staff"
+ },
+ {
+ "name": "Keep information from patient"
+ },
+ {
+ "name": "Contact/Employment Details Confidential"
+ },
+ {
+ "name": "Diagnosis-related confidentiality"
+ },
+ {
+ "name": "Author consent needed"
+ },
+ {
+ "name": "Delete After Use"
+ },
+ {
+ "name": "Do Not Reuse"
+ },
+ {
+ "name": "Break The Glass"
+ },
+ {
+ "name": "Confidentiality Classification"
+ },
+ {
+ "name": "Sensitivity Category"
+ },
+ {
+ "name": "Compartment Category"
+ },
+ {
+ "name": "Integrity Category"
+ },
+ {
+ "name": "Handling Caveat"
+ },
+ {
+ "name": "US Privacy Law"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "MESSAGING",
+ "children": [
+ {
+ "name": "Consequence"
+ },
+ {
+ "name": "Currency"
+ },
+ {
+ "name": "Notification"
+ }
+ ]
+ },
+ {
+ "name": "DOCUMENTS",
+ "children": [
+
+ ]
+ },
+ {
+ "name": "PROFILES",
+ "children": [
+ {
+ "name": "validate-profile"
+ }
+ ]
+ },
+ {
+ "name": "EXTENSIONS",
+ "children": [
+ {
+ "name": "extensions"
+ },
+ {
+ "name": "modifying extensions"
+ },
+ {
+ "name": "complex extensions"
+ },
+ {
+ "name": "primitive extensions"
+ }
+ ]
+ }
+ ]
+}
diff --git a/lib/data/fhir_structure.rb b/lib/data/fhir_structure.rb
index 8e2f43bc..2bc1b984 100644
--- a/lib/data/fhir_structure.rb
+++ b/lib/data/fhir_structure.rb
@@ -1,6 +1,6 @@
module Crucible
class FHIRStructure
- def self.get(fhir_version = :r4)
+ def self.get(fhir_version)
root = File.expand_path File.join('..','..'), File.dirname(File.absolute_path(__FILE__))
JSON.parse(File.read(File.join(root, 'lib', "FHIR_structure_#{fhir_version.to_s}.json")))
end
diff --git a/lib/data/fhir_structure_generator.rb b/lib/data/fhir_structure_generator.rb
new file mode 100644
index 00000000..146eaf01
--- /dev/null
+++ b/lib/data/fhir_structure_generator.rb
@@ -0,0 +1,93 @@
+require 'cgi'
+require 'digest'
+require 'open3'
+
+module Crucible
+ class FHIRStructureGenerator
+ DEFINITIONS_URL = 'https://www.hl7.org/fhir/R4B/definitions.json.zip'
+ DEFINITIONS_SHA256 = 'a2793a06853c2d4540db8a72fc1c6d972528b01d113c2bb70ae2d80dc062e963'
+ PROFILES_ENTRY = 'definitions.json/profiles-resources.json'
+ CATEGORY_URL = 'http://hl7.org/fhir/StructureDefinition/structuredefinition-category'
+ # These two base definitions omit the category extension in the official archive.
+ CATEGORY_OVERRIDES = {
+ 'ResearchDefinition' => 'Specialized.Evidence-Based Medicine',
+ 'ResearchElementDefinition' => 'Specialized.Evidence-Based Medicine'
+ }.freeze
+
+ def self.from_archive(archive_path, template_path)
+ checksum = Digest::SHA256.file(archive_path).hexdigest
+ raise "Unexpected R4B definitions checksum: #{checksum}" unless checksum == DEFINITIONS_SHA256
+
+ profiles_json, status = Open3.capture2('unzip', '-p', archive_path, PROFILES_ENTRY)
+ raise "Unable to read #{PROFILES_ENTRY} from #{archive_path}" unless status.success?
+
+ generate(JSON.parse(profiles_json), JSON.parse(File.read(template_path)))
+ end
+
+ def self.generate(structure_definitions, template)
+ result = JSON.parse(JSON.generate(template))
+ resource_root = result.fetch('children').find { |child| child['name'] == 'RESOURCES' }
+ raise 'FHIR structure template has no RESOURCES branch' unless resource_root
+
+ categories = reset_resource_categories(resource_root)
+ concrete_resources(structure_definitions).each do |resource|
+ category_path = resource_category(resource)
+ category = categories[category_path] || add_category(resource_root, categories, category_path)
+ category.fetch('children') << { 'name' => humanize(resource.fetch('name')) }
+ end
+ result
+ end
+
+ def self.write_from_archive(archive_path, template_path, output_path)
+ structure = from_archive(archive_path, template_path)
+ File.write(output_path, "#{JSON.pretty_generate(structure)}\n")
+ end
+
+ def self.reset_resource_categories(resource_root)
+ resource_root.fetch('children').each_with_object({}) do |section, categories|
+ section.fetch('children').each do |category|
+ category['children'] = []
+ categories["#{section.fetch('name')}.#{category.fetch('name')}"] = category
+ end
+ end
+ end
+ private_class_method :reset_resource_categories
+
+ def self.concrete_resources(structure_definitions)
+ structure_definitions.fetch('entry').map { |entry| entry.fetch('resource') }
+ .select { |resource| resource['kind'] == 'resource' && resource['derivation'] == 'specialization' }
+ .reject { |resource| resource['abstract'] == true }
+ end
+ private_class_method :concrete_resources
+
+ def self.resource_category(resource)
+ extension = resource.fetch('extension', []).find { |item| item['url'] == CATEGORY_URL }
+ category = extension && extension['valueString']
+ category ||= CATEGORY_OVERRIDES[resource.fetch('name')]
+ raise "No R4B resource category for #{resource.fetch('name')}" unless category
+
+ CGI.unescapeHTML(category)
+ end
+ private_class_method :resource_category
+
+ def self.add_category(resource_root, categories, category_path)
+ section_name, category_name = category_path.split('.', 2)
+ raise "Invalid R4B resource category: #{category_path}" unless category_name
+
+ section = resource_root.fetch('children').find { |child| child['name'] == section_name }
+ unless section
+ section = { 'name' => section_name, 'children' => [] }
+ resource_root.fetch('children') << section
+ end
+ category = { 'name' => category_name, 'children' => [] }
+ section.fetch('children') << category
+ categories[category_path] = category
+ end
+ private_class_method :add_category
+
+ def self.humanize(name)
+ name.gsub(/([a-z\d])([A-Z])/, '\\1 \\2').downcase
+ end
+ private_class_method :humanize
+ end
+end
diff --git a/lib/data/resources.rb b/lib/data/resources.rb
index 0ff889e1..7db34a87 100644
--- a/lib/data/resources.rb
+++ b/lib/data/resources.rb
@@ -4,11 +4,9 @@ class Resources
FIXTURE_DIR = File.join(File.expand_path(File.join('..','..','..'),File.absolute_path(__FILE__)), 'fixtures')
- def initialize(fhir_version = nil)
+ def initialize(fhir_version)
@fhir_version = fhir_version
- @namespace = FHIR
- @namespace = FHIR::DSTU2 if @fhir_version == :dstu2
- @namespace = FHIR::STU3 if @fhir_version == :stu3
+ @namespace = Crucible::FHIRVersion.namespace(@fhir_version)
end
def example_patient
@@ -243,7 +241,8 @@ def tag_metadata(resource)
def load_fixture(path, extension)
full_path = File.join(fixture_path, "#{path}.#{extension.to_s}")
- full_path = File.join(fixture_path, "#{path}.#{@fhir_version.to_s}.#{extension}") if File.exist?(File.join("#{path}.#{@fhir_version.to_s}.#{extension}"))
+ versioned_path = File.join(fixture_path, "#{path}.#{@fhir_version}.#{extension}")
+ full_path = versioned_path if File.exist?(versioned_path)
tag_metadata(@namespace.from_contents(File.read(full_path)))
end
diff --git a/lib/ext/client.rb b/lib/ext/client.rb
index 8597b7f7..cd5f4d04 100644
--- a/lib/ext/client.rb
+++ b/lib/ext/client.rb
@@ -9,12 +9,6 @@ def record_requests(reply)
@requests << reply
end
- def use_fhir_version(fhir_version)
- self.use_r4
- self.use_stu3 if fhir_version == :stu3
- self.use_dstu2 if fhir_version == :dstu2
- end
-
def monitor_requests
return if @decorated
@decorated = true
@@ -53,37 +47,7 @@ def set_client_secrets(options)
end
def capability_statement_new(format = @default_format)
- if !@cached_capability_statement.nil? && format == @default_format
- return @cached_capability_statement
- end
-
- formats = [FHIR::Formats::ResourceFormat::RESOURCE_XML,
- FHIR::Formats::ResourceFormat::RESOURCE_JSON,
- FHIR::Formats::ResourceFormat::RESOURCE_XML_DSTU2,
- FHIR::Formats::ResourceFormat::RESOURCE_JSON_DSTU2,
- 'application/xml',
- 'application/json']
- formats.insert(0, format)
-
- @cached_capability_statement = nil
-
- formats.each do |frmt|
- reply = get 'metadata', fhir_headers({ accept: "#{frmt}" })
- next unless reply.code == 200
- begin
- @cached_capability_statement = parse_reply(FHIR::DSTU2::Conformance, frmt, reply) if @fhir_version == :dstu2
- @cached_capability_statement = parse_reply(FHIR::STU3::CapabilityStatement, frmt, reply) if @fhir_version == :stu3
- @cached_capability_statement = parse_reply(FHIR::CapabilityStatement, frmt, reply) if @fhir_version != :dstu2 && @fhir_version != :stu3
- rescue
- @cached_capability_statement = nil
- end
- if @cached_capability_statement
- @default_format = frmt
- break
- end
- end
- @default_format = format if @default_format.nil?
- @cached_capability_statement
+ capability_statement(format)
end
def fhir_patch(klass, id, patchset, options = {}, format = nil, additional_header = {})
diff --git a/lib/fhir_version.rb b/lib/fhir_version.rb
new file mode 100644
index 00000000..b9587659
--- /dev/null
+++ b/lib/fhir_version.rb
@@ -0,0 +1,47 @@
+module Crucible
+ module FHIRVersion
+ NAMESPACES = {
+ dstu2: 'FHIR::DSTU2',
+ stu3: 'FHIR::STU3',
+ r4: 'FHIR',
+ r4b: 'FHIR::R4B'
+ }.freeze
+ KNOWN = NAMESPACES.keys.freeze
+
+ class UnsupportedVersionError < ArgumentError; end
+
+ def self.resolve(value)
+ normalized = value.to_s.strip.downcase
+ if normalized.empty?
+ raise UnsupportedVersionError,
+ "FHIR version is required. Supported versions: #{KNOWN.join(', ')}"
+ end
+
+ version = normalized.to_sym
+ return version if KNOWN.include?(version)
+
+ raise UnsupportedVersionError,
+ "Unsupported FHIR version '#{value}'. Supported versions: #{KNOWN.join(', ')}"
+ end
+
+ def self.namespace(value)
+ namespace_name(value).split('::').inject(Object) do |parent, name|
+ parent.const_get(name)
+ end
+ end
+
+ def self.namespace_name(value)
+ NAMESPACES.fetch(resolve(value))
+ end
+
+ def self.for_class(value)
+ class_name = value.is_a?(Module) ? value.name : value.class.name
+ version = NAMESPACES.sort_by { |_key, name| -name.length }.find do |_key, name|
+ class_name == name || class_name.start_with?("#{name}::")
+ end
+ return version.first if version
+
+ raise UnsupportedVersionError, "Unable to determine FHIR version for #{class_name}"
+ end
+ end
+end
diff --git a/lib/plan_executor.rb b/lib/plan_executor.rb
index 1c372a6a..bdfa8f2d 100644
--- a/lib/plan_executor.rb
+++ b/lib/plan_executor.rb
@@ -12,6 +12,7 @@
require 'active_support/core_ext'
require 'jsonpath'
+require_relative File.join('.','fhir_version.rb')
require_relative File.join('.','executor.rb')
require_relative File.join('.','test_result.rb')
require_relative File.join('.','resource_generator.rb')
diff --git a/lib/resource_generator.rb b/lib/resource_generator.rb
index 0b3fac5c..5f3cf77c 100644
--- a/lib/resource_generator.rb
+++ b/lib/resource_generator.rb
@@ -14,9 +14,7 @@ class ResourceGenerator
#
def self.generate(klass,embedded=0)
resource = klass.new
- namespace = 'FHIR'
- namespace = 'FHIR::DSTU2' if klass.name.starts_with? 'FHIR::DSTU2'
- namespace = 'FHIR::STU3' if klass.name.starts_with? 'FHIR::STU3'
+ namespace = Crucible::FHIRVersion.namespace_name(Crucible::FHIRVersion.for_class(klass))
Time.zone = 'UTC'
set_fields!(resource, namespace, embedded)
resource.id=nil if resource.respond_to?(:id=)
@@ -65,7 +63,7 @@ def self.set_fields!(resource, namespace, embedded=0)
gen = SecureRandom.uuid
elsif type == 'code'
if meta['valid_codes']
- gen = meta['valid_codes'].values.first.sample
+ gen = selectable_valid_codes(meta, namespace).values.flatten.sample
elsif meta['binding'] && ['http://tools.ietf.org/html/bcp47','http://hl7.org/fhir/ValueSet/languages'].include?(meta['binding']['uri'])
gen = 'en-US'
elsif meta['binding'] && ['http://www.rfc-editor.org/bcp/bcp13.txt','http://hl7.org/fhir/ValueSet/content-type'].include?(meta['binding']['uri'])
@@ -110,9 +108,10 @@ def self.set_fields!(resource, namespace, embedded=0)
gen = generate_child(type, namespace, embedded-1)
# apply bindings
if type == 'CodeableConcept' && meta['valid_codes'] && meta['binding']
+ valid_codes = selectable_valid_codes(meta, namespace)
gen.coding.each do |c|
- c.system = meta['valid_codes'].keys.sample
- c.code = meta['valid_codes'][c.system].sample
+ c.system = valid_codes.keys.sample
+ c.code = valid_codes[c.system].sample
display = "#{namespace}::Definitions".constantize.get_display(c.system, c.code) if "#{namespace}::Definitions".constantize.respond_to?('get_display')
c.display = display ? display : nil
end
@@ -124,8 +123,9 @@ def self.set_fields!(resource, namespace, embedded=0)
elsif type == 'CodeableConcept' && meta['binding'] && meta['binding']['strength'] == 'required' && !meta['valid_codes'] && meta['min'] == 0
gen = nil # Cannot generate valid code for external required binding (e.g. LOINC/SNOMED); field is optional so safe to skip
elsif type == 'Coding' && meta['valid_codes'] && meta['binding']
- gen.system = meta['valid_codes'].keys.sample
- gen.code = meta['valid_codes'][gen.system].sample
+ valid_codes = selectable_valid_codes(meta, namespace)
+ gen.system = valid_codes.keys.sample
+ gen.code = valid_codes[gen.system].sample
display = "#{namespace}::Definitions".constantize.get_display(gen.system, gen.code) if "#{namespace}::Definitions".constantize.respond_to?('get_display')
gen.display = display ? display : nil
elsif type == 'Reference'
@@ -161,6 +161,49 @@ def self.set_fields!(resource, namespace, embedded=0)
resource
end
+ def self.selectable_valid_codes(meta, namespace)
+ valid_codes = meta['valid_codes']
+ binding_uri = meta.dig('binding', 'uri')
+ return valid_codes unless binding_uri
+
+ definitions = "#{namespace}::Definitions".constantize
+ return valid_codes unless definitions.respond_to?(:expansions)
+
+ @selectable_expansion_codes_cache ||= {}
+ normalized_uri = binding_uri.sub(/\|[A-Za-z0-9.\-]+\z/, '')
+ cache_key = [namespace, normalized_uri]
+ selectable_codes = @selectable_expansion_codes_cache[cache_key]
+ unless @selectable_expansion_codes_cache.key?(cache_key)
+ value_set = definitions.expansions.find { |resource| resource['url'] == normalized_uri }
+ selectable_codes = if value_set
+ collect_selectable_expansion_codes(
+ value_set.dig('expansion', 'contains'),
+ {}
+ )
+ end
+ @selectable_expansion_codes_cache[cache_key] = selectable_codes
+ end
+ return valid_codes unless selectable_codes
+
+ filtered_codes = valid_codes.each_with_object({}) do |(system, codes), filtered|
+ selectable = codes & selectable_codes.fetch(system, [])
+ filtered[system] = selectable unless selectable.empty?
+ end
+
+ filtered_codes.empty? ? valid_codes : filtered_codes
+ end
+
+ def self.collect_selectable_expansion_codes(entries, codes, inherited_system = nil)
+ entries.to_a.each do |entry|
+ system = entry['system'] || inherited_system
+ if system && entry['code'] && entry['abstract'] != true && entry['inactive'] != true
+ (codes[system] ||= []) << entry['code']
+ end
+ collect_selectable_expansion_codes(entry['contains'], codes, system)
+ end
+ codes
+ end
+
def self.ancestor_fhir_classes(klass,namespace)
classes = klass.constants
classes.concat ancestor_fhir_classes(klass.module_parent, namespace) if klass.module_parent.name != namespace && klass.module_parent != Object
@@ -182,11 +225,11 @@ def self.random_oid
oid
end
- def self.minimal_patient(identifier='0',name='Name', namespace = FHIR)
+ def self.minimal_patient(identifier='0', name='Name', namespace:)
resource = namespace.const_get(:Patient).new
- resource.identifier = [ minimal_identifier(identifier) ]
- resource.name = [ minimal_humanname(name) ]
- tag_metadata(resource)
+ resource.identifier = [minimal_identifier(identifier, namespace: namespace)]
+ resource.name = [minimal_humanname(name, namespace: namespace)]
+ tag_metadata(resource, namespace: namespace)
end
# Common systems:
@@ -194,21 +237,21 @@ def self.minimal_patient(identifier='0',name='Name', namespace = FHIR)
# LOINC http://loinc.org
# ICD-10 http://hl7.org/fhir/sid/icd-10
# units: must be UCOM
- def self.minimal_observation(system='http://loinc.org',code='8302-2',value=170,units='cm',patientId=nil, namespace = FHIR)
+ def self.minimal_observation(system='http://loinc.org', code='8302-2', value=170, units='cm', patientId=nil, namespace:)
resource = namespace.const_get(:Observation).new
resource.status = 'final'
- resource.code = minimal_codeableconcept(system,code, namespace)
+ resource.code = minimal_codeableconcept(system, code, namespace: namespace)
if patientId
ref = namespace.const_get(:Reference).new
ref.reference = "Patient/#{patientId}"
resource.subject = ref
end
- resource.valueQuantity = minimal_quantity(value,units, namespace)
- tag_metadata(resource)
+ resource.valueQuantity = minimal_quantity(value, units, namespace: namespace)
+ tag_metadata(resource, namespace: namespace)
end
# Default system/code are for SNOMED "Obese (finding)"
- def self.minimal_condition(system='http://snomed.info/sct',code='414915002',patientId=nil, namespace = FHIR, patientRef=nil)
+ def self.minimal_condition(system='http://snomed.info/sct', code='414915002', patientId=nil, namespace:, patient_ref: nil)
resource = namespace.const_get(:Condition).new
if resource.is_a?(FHIR::DSTU2::Condition)
resource.patient = namespace.const_get(:Reference).new
@@ -217,8 +260,8 @@ def self.minimal_condition(system='http://snomed.info/sct',code='414915002',pati
else
resource.patient.display = 'Patient'
end
- if (patientRef)
- resource.patient.reference = patientRef
+ if patient_ref
+ resource.patient.reference = patient_ref
end
else
resource.subject = namespace.const_get(:Reference).new
@@ -227,17 +270,17 @@ def self.minimal_condition(system='http://snomed.info/sct',code='414915002',pati
else
resource.subject.display = 'Patient'
end
- if (patientRef)
- resource.subject.reference = patientRef
+ if patient_ref
+ resource.subject.reference = patient_ref
end
end
- resource.code = minimal_codeableconcept(system,code, namespace)
+ resource.code = minimal_codeableconcept(system, code, namespace: namespace)
resource.verificationStatus = 'confirmed'
fix_condition(resource)
- tag_metadata(resource)
+ tag_metadata(resource, namespace: namespace)
end
- def self.minimal_identifier(identifier='0', namespace = FHIR)
+ def self.minimal_identifier(identifier='0', namespace:)
mid = namespace.const_get(:Identifier).new
mid.use = 'official'
mid.system = 'http://projectcrucible.org'
@@ -245,7 +288,7 @@ def self.minimal_identifier(identifier='0', namespace = FHIR)
mid
end
- def self.minimal_humanname(name='Name', namespace = FHIR)
+ def self.minimal_humanname(name='Name', namespace:)
hn = namespace.const_get(:HumanName).new
hn.use = 'official'
hn.family = 'Crucible'
@@ -254,13 +297,13 @@ def self.minimal_humanname(name='Name', namespace = FHIR)
hn
end
- def self.textonly_codeableconcept(text='text', namespace = FHIR)
+ def self.textonly_codeableconcept(text='text', namespace:)
concept = namespace.const_get(:CodeableConcept).new
concept.text = text
concept
end
- def self.textonly_reference(text='Reference', namespace = FHIR)
+ def self.textonly_reference(text='Reference', namespace:)
ref = namespace.const_get(:Reference).new
ref.display = "#{text} #{SecureRandom.base64}"
ref
@@ -270,9 +313,9 @@ def self.textonly_reference(text='Reference', namespace = FHIR)
# SNOMED http://snomed.info/sct
# LOINC http://loinc.org
# ICD-10 http://hl7.org/fhir/sid/icd-10
- def self.minimal_codeableconcept(system='http://loinc.org',code='8302-2', namespace = FHIR)
+ def self.minimal_codeableconcept(system='http://loinc.org', code='8302-2', namespace:)
concept = namespace.const_get(:CodeableConcept).new
- concept.coding = [ minimal_coding(system,code,namespace) ]
+ concept.coding = [minimal_coding(system, code, namespace: namespace)]
concept
end
@@ -280,14 +323,14 @@ def self.minimal_codeableconcept(system='http://loinc.org',code='8302-2', namesp
# SNOMED http://snomed.info/sct
# LOINC http://loinc.org
# ICD-10 http://hl7.org/fhir/sid/icd-10
- def self.minimal_coding(system='http://loinc.org',code='8302-2',namespace = FHIR)
+ def self.minimal_coding(system='http://loinc.org', code='8302-2', namespace:)
coding = namespace.const_get(:Coding).new
coding.system = system
coding.code = code
coding
end
- def self.minimal_quantity(value=170,units='cm', namespace = FHIR)
+ def self.minimal_quantity(value=170, units='cm', namespace:)
quantity = namespace.const_get(:Quantity).new
quantity.value = value
quantity.unit = units
@@ -295,43 +338,58 @@ def self.minimal_quantity(value=170,units='cm', namespace = FHIR)
quantity
end
- def self.minimal_animal(namespace = FHIR)
+ def self.minimal_animal(namespace:)
animal = namespace.const_get(:Patient).const_get(:Animal).new
- animal.species = minimal_codeableconcept('http://hl7.org/fhir/animal-species','canislf', namespaec) # dog
- animal.breed = minimal_codeableconcept('http://hl7.org/fhir/animal-breed','gret', namespaec) # golden retriever
- animal.genderStatus = minimal_codeableconcept('http://hl7.org/fhir/animal-genderstatus','intact', namespaec) # intact
+ animal.species = minimal_codeableconcept('http://hl7.org/fhir/animal-species', 'canislf', namespace: namespace) # dog
+ animal.breed = minimal_codeableconcept('http://hl7.org/fhir/animal-breed', 'gret', namespace: namespace) # golden retriever
+ animal.genderStatus = minimal_codeableconcept('http://hl7.org/fhir/animal-genderstatus', 'intact', namespace: namespace) # intact
animal
end
- def self.tag_metadata(resource, namespace = FHIR)
+ def self.tag_metadata(resource, namespace:)
return nil unless resource
if resource.meta.nil?
resource.meta = namespace.const_get(:Meta).new({ 'tag' => [{'system'=>'http://projectcrucible.org', 'code'=>'testdata'}]})
else
- resource.meta.tag << @namespace.const_get(:Coding).new({'system'=>'http://projectcrucible.org', 'code'=>'testdata'})
+ resource.meta.tag << namespace.const_get(:Coding).new({'system'=>'http://projectcrucible.org', 'code'=>'testdata'})
end
resource
end
+ def self.fix_codeable_reference(resource)
+ namespace = Crucible::FHIRVersion.namespace(Crucible::FHIRVersion.for_class(resource))
+ return resource unless namespace.const_defined?(:CodeableReference, false)
+ return resource unless resource.is_a?(namespace.const_get(:CodeableReference))
+ return resource if resource.concept || resource.reference
+
+ resource.concept = textonly_codeableconcept('Generated CodeableReference', namespace: namespace)
+ resource
+ end
+
def self.fix_condition(resource)
- if resource.is_a?(FHIR::Condition)
- # They changed it from `code` data type to `CodeableConcept` in `R4`
- if resource.clinicalStatus.kind_of? String
- resource.clinicalStatus = minimal_codeableconcept(
- 'http://terminology.hl7.org/CodeSystem/condition-clinical',
- resource.clinicalStatus)
- end
- if resource.verificationStatus.kind_of? String
- resource.verificationStatus = minimal_codeableconcept(
- 'http://terminology.hl7.org/CodeSystem/condition-ver-status',
- resource.verificationStatus)
- end
+ version = Crucible::FHIRVersion.for_class(resource)
+ return resource unless [:r4, :r4b].include?(version)
+
+ namespace = Crucible::FHIRVersion.namespace(version)
+ if resource.clinicalStatus.kind_of? String
+ resource.clinicalStatus = minimal_codeableconcept(
+ 'http://terminology.hl7.org/CodeSystem/condition-clinical',
+ resource.clinicalStatus,
+ namespace: namespace)
+ end
+ if resource.verificationStatus.kind_of? String
+ resource.verificationStatus = minimal_codeableconcept(
+ 'http://terminology.hl7.org/CodeSystem/condition-ver-status',
+ resource.verificationStatus,
+ namespace: namespace)
end
resource
end
def self.apply_invariants!(resource)
+ fix_codeable_reference(resource)
+
case resource
when FHIR::ActivityDefinition
resource.quantity.comparator = nil unless resource.quantity.nil?
@@ -351,10 +409,10 @@ def self.apply_invariants!(resource)
resource.unit = nil
resource.comparator = nil
when FHIR::Appointment
- resource.reasonCode = [ minimal_codeableconcept('http://snomed.info/sct','219006') ] # drinker of alcohol
- resource.participant.each{|p| p.type=[ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency') ] }
+ resource.reasonCode = [ minimal_codeableconcept('http://snomed.info/sct','219006', namespace: FHIR) ] # drinker of alcohol
+ resource.participant.each{|p| p.type=[ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', namespace: FHIR) ] }
when FHIR::AppointmentResponse
- resource.participantType = [ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency') ]
+ resource.participantType = [ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', namespace: FHIR) ]
when FHIR::AuditEvent
resource.entity.each do |o|
o.query=nil
@@ -421,18 +479,18 @@ def self.apply_invariants!(resource)
end
when FHIR::ClaimResponse
resource.item.each do |item|
- item.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit')}
+ item.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR)}
item.detail.each do |detail|
- detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit')}
+ detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR)}
detail.subDetail.each do |sub|
- sub.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit')}
+ sub.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR)}
end
end
end
resource.addItem.each do |addItem|
- addItem.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit')}
+ addItem.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR)}
addItem.detail.each do |detail|
- detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit')}
+ detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR)}
end
end
when FHIR::Communication
@@ -590,7 +648,7 @@ def self.apply_invariants!(resource)
code = nil
end
resource.outcomeReference.each do |reference|
- reference = textonly_reference('Observation')
+ reference = textonly_reference('Observation', namespace: FHIR)
end
resource.target.each do |target|
unless target.dueDuration.nil?
@@ -607,7 +665,7 @@ def self.apply_invariants!(resource)
availability = ['ONLINE', 'OFFLINE', 'NEARLINE', 'UNAVAILABLE']
resource.series.each do |series|
series.instance.each do |instance|
- instance.sopClass = minimal_coding('urn:ietf:rfc:3986', random_oid)
+ instance.sopClass = minimal_coding('urn:ietf:rfc:3986', random_oid, namespace: FHIR)
end
end
when FHIR::Immunization
@@ -645,25 +703,25 @@ def self.apply_invariants!(resource)
date = DateTime.now
resource.effectiveDateTime = date.strftime("%Y-%m-%dT%T.%LZ")
resource.effectivePeriod = nil
- resource.medicationReference = textonly_reference('Medication')
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR)
resource.medicationCodeableConcept = nil
unless resource.dosage.nil?
resource.dosage.dose.comparator = nil unless resource.dosage.dose.nil?
resource.dosage.rateQuantity = nil
end
when FHIR::MedicationDispense
- resource.medicationReference = textonly_reference('Medication')
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR)
resource.medicationCodeableConcept = nil
resource.dosageInstruction.each {|d|d.timing = nil }
resource.quantity.comparator = nil unless resource.quantity.nil?
resource.daysSupply.comparator = nil unless resource.daysSupply.nil?
when FHIR::MedicationRequest
- resource.medicationReference = textonly_reference('Medication')
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR)
resource.medicationCodeableConcept = nil
resource.dosageInstruction.each {|d|d.timing = nil }
resource.dispenseRequest.quantity.comparator = nil if resource&.dispenseRequest&.quantity != nil
when FHIR::MedicationStatement
- resource.medicationReference = textonly_reference('Medication')
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR)
resource.medicationCodeableConcept = nil
resource.dosage.each{|d|d.timing=nil}
when FHIR::MessageDefinition
@@ -721,7 +779,7 @@ def self.apply_invariants!(resource)
p.searchType = nil unless p.type == 'string'
end
when FHIR::Patient
- resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S')
+ resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S', namespace: FHIR)
when FHIR::PlanDefinition
resource.action.each do |a|
a.action.each do |b|
@@ -731,7 +789,7 @@ def self.apply_invariants!(resource)
when FHIR::Procedure
resource.focalDevice.each do |fd|
code = ['implanted', 'explanted', 'manipulated'].sample
- fd.action = minimal_codeableconcept('http://hl7.org/fhir/device-action', code)
+ fd.action = minimal_codeableconcept('http://hl7.org/fhir/device-action', code, namespace: FHIR)
end
when FHIR::Provenance
resource.entity.each do |e|
@@ -745,7 +803,7 @@ def self.apply_invariants!(resource)
end
end
when FHIR::RelatedPerson
- resource.relationship = [minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','family')]
+ resource.relationship = [minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','family', namespace: FHIR)]
when FHIR::Questionnaire
# resource.item.each do |i|
# i.required = true
@@ -797,7 +855,7 @@ def self.apply_invariants!(resource)
when FHIR::SampledData
resource.origin.comparator = nil unless resource.origin.nil?
when FHIR::Signature
- resource.type = [ minimal_coding('urn:iso-astm:E1762-95:2013','1.2.840.10065.1.12.1.18') ]
+ resource.type = [ minimal_coding('urn:iso-astm:E1762-95:2013','1.2.840.10065.1.12.1.18', namespace: FHIR) ]
resource.targetFormat = nil
resource.sigFormat = nil
when FHIR::Specimen
@@ -817,10 +875,10 @@ def self.apply_invariants!(resource)
instance.quantity.comparator = nil unless instance.quantity.nil?
end
when FHIR::SupplyDelivery
- resource.type = minimal_codeableconcept('http://hl7.org/fhir/supply-item-type','medication')
+ resource.type = minimal_codeableconcept('http://hl7.org/fhir/supply-item-type','medication', namespace: FHIR)
resource.suppliedItem.quantity.comparator = nil if !resource.suppliedItem.nil? && !resource.suppliedItem.quantity.nil?
when FHIR::SupplyRequest
- resource.category = minimal_codeableconcept('http://hl7.org/fhir/supply-kind','central')
+ resource.category = minimal_codeableconcept('http://hl7.org/fhir/supply-kind','central', namespace: FHIR)
when FHIR::StructureDefinition
resource.derivation = 'constraint'
resource.fhirVersion = '4.0.0'
@@ -1001,10 +1059,10 @@ def self.apply_invariants!(resource)
resource.unit = nil
resource.comparator = nil
when FHIR::STU3::Appointment
- resource.reason = [ minimal_codeableconcept('http://snomed.info/sct','219006', FHIR::STU3) ] # drinker of alcohol
- resource.participant.each{|p| p.type=[ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', FHIR::STU3) ] }
+ resource.reason = [ minimal_codeableconcept('http://snomed.info/sct','219006', namespace: FHIR::STU3) ] # drinker of alcohol
+ resource.participant.each{|p| p.type=[ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', namespace: FHIR::STU3) ] }
when FHIR::STU3::AppointmentResponse
- resource.participantType = [ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', FHIR::STU3) ]
+ resource.participantType = [ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', namespace: FHIR::STU3) ]
when FHIR::STU3::AuditEvent
resource.entity.each do |o|
o.query=nil
@@ -1067,7 +1125,7 @@ def self.apply_invariants!(resource)
end
when FHIR::STU3::Claim
resource.item.each do |item|
- item.category = minimal_codeableconcept('http://hl7.org/fhir/benefit-subcategory','35', FHIR::STU3)
+ item.category = minimal_codeableconcept('http://hl7.org/fhir/benefit-subcategory','35', namespace: FHIR::STU3)
item.quantity.comparator = nil unless item.quantity.nil?
item.detail.each do |detail|
detail.category = item.category
@@ -1075,24 +1133,24 @@ def self.apply_invariants!(resource)
detail.subDetail.each do |sub|
sub.category = item.category
sub.quantity.comparator = nil unless sub.quantity.nil?
- sub.service = minimal_codeableconcept('http://hl7.org/fhir/ex-USCLS','1205', FHIR::STU3)
+ sub.service = minimal_codeableconcept('http://hl7.org/fhir/ex-USCLS','1205', namespace: FHIR::STU3)
end
end
end
when FHIR::STU3::ClaimResponse
resource.item.each do |item|
- item.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', FHIR::STU3)}
+ item.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::STU3)}
item.detail.each do |detail|
- detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', FHIR::STU3)}
+ detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::STU3)}
detail.subDetail.each do |sub|
- sub.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', FHIR::STU3)}
+ sub.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::STU3)}
end
end
end
resource.addItem.each do |addItem|
- addItem.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', FHIR::STU3)}
+ addItem.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::STU3)}
addItem.detail.each do |detail|
- detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', FHIR::STU3)}
+ detail.adjudication.each{|a|a.category = minimal_codeableconcept('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::STU3)}
end
end
when FHIR::STU3::Communication
@@ -1111,10 +1169,10 @@ def self.apply_invariants!(resource)
end
when FHIR::STU3::ConceptMap
if(resource.sourceUri.nil? && resource.sourceReference.nil?)
- resource.sourceReference = textonly_reference('ValueSet', FHIR::STU3)
+ resource.sourceReference = textonly_reference('ValueSet', namespace: FHIR::STU3)
end
if(resource.targetUri.nil? && resource.targetReference.nil?)
- resource.targetReference = textonly_reference('ValueSet', FHIR::STU3)
+ resource.targetReference = textonly_reference('ValueSet', namespace: FHIR::STU3)
end
when FHIR::STU3::Condition
if resource.onsetAge
@@ -1149,7 +1207,7 @@ def self.apply_invariants!(resource)
resource.messaging.each{|m| m.endpoint = nil} if resource.kind != 'instance'
when FHIR::STU3::Contract
resource.agent.each do |agent|
- agent.actor = textonly_reference('Patient', FHIR::STU3)
+ agent.actor = textonly_reference('Patient', namespace: FHIR::STU3)
end
resource.valuedItem.each do |item|
if item.unitPrice
@@ -1168,11 +1226,11 @@ def self.apply_invariants!(resource)
end
resource.term.each do |term|
term.agent.each do |agent|
- agent.actor = textonly_reference('Organization', FHIR::STU3)
+ agent.actor = textonly_reference('Organization', namespace: FHIR::STU3)
end
term.group.each do |group|
group.agent.each do |agent|
- agent.actor = textonly_reference('Organization', FHIR::STU3)
+ agent.actor = textonly_reference('Organization', namespace: FHIR::STU3)
end
end
term.valuedItem.each do |item|
@@ -1193,15 +1251,15 @@ def self.apply_invariants!(resource)
end
resource.friendly.each do |f|
f.contentAttachment = nil
- f.contentReference = textonly_reference('DocumentReference', FHIR::STU3)
+ f.contentReference = textonly_reference('DocumentReference', namespace: FHIR::STU3)
end
resource.legal.each do |f|
f.contentAttachment = nil
- f.contentReference = textonly_reference('DocumentReference', FHIR::STU3)
+ f.contentReference = textonly_reference('DocumentReference', namespace: FHIR::STU3)
end
resource.rule.each do |f|
f.contentAttachment = nil
- f.contentReference = textonly_reference('DocumentReference', FHIR::STU3)
+ f.contentReference = textonly_reference('DocumentReference', namespace: FHIR::STU3)
end
when FHIR::STU3::DataElement
resource.element.each do |e|
@@ -1230,7 +1288,7 @@ def self.apply_invariants!(resource)
when FHIR::STU3::DocumentManifest
resource.content.each do |c|
c.pAttachment = nil
- c.pReference = textonly_reference('Any', FHIR::STU3)
+ c.pReference = textonly_reference('Any', namespace: FHIR::STU3)
end
when FHIR::STU3::DocumentReference
resource.docStatus = 'preliminary'
@@ -1324,7 +1382,7 @@ def self.apply_invariants!(resource)
code = nil
end
resource.outcomeReference.each do |reference|
- reference = textonly_reference('Observation', FHIR::STU3)
+ reference = textonly_reference('Observation', namespace: FHIR::STU3)
end
if resource.target && resource.target.dueDuration
resource.target.dueDuration.system = 'http://unitsofmeasure.org'
@@ -1365,14 +1423,14 @@ def self.apply_invariants!(resource)
resource.doseQuantity.comparator = nil unless resource.doseQuantity.nil?
if resource.notGiven
unless resource.explanation.nil?
- resource.explanation.reasonNotGiven = [ textonly_codeableconcept("reasonNotGiven #{SecureRandom.base64}", FHIR::STU3) ]
+ resource.explanation.reasonNotGiven = [ textonly_codeableconcept("reasonNotGiven #{SecureRandom.base64}", namespace: FHIR::STU3) ]
resource.explanation.reason = nil
end
resource.reaction = nil
else
unless resource.explanation.nil?
resource.explanation.reasonNotGiven = nil
- resource.explanation.reason = [ textonly_codeableconcept("reason #{SecureRandom.base64}", FHIR::STU3) ]
+ resource.explanation.reason = [ textonly_codeableconcept("reason #{SecureRandom.base64}", namespace: FHIR::STU3) ]
end
end
resource.status = ['completed','entered-in-error'].sample
@@ -1381,7 +1439,7 @@ def self.apply_invariants!(resource)
resource.package.each do |package|
package.resource.each do |r|
r.sourceUri = nil
- r.sourceReference = textonly_reference('Any', FHIR::STU3)
+ r.sourceReference = textonly_reference('Any', namespace: FHIR::STU3)
end
end
when FHIR::STU3::Linkage
@@ -1425,26 +1483,26 @@ def self.apply_invariants!(resource)
else
resource.reasonNotGiven = nil
end
- resource.medicationReference = textonly_reference('Medication', FHIR::STU3)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::STU3)
resource.medicationCodeableConcept = nil
unless resource.dosage.nil?
resource.dosage.dose.comparator = nil unless resource.dosage.dose.nil?
resource.dosage.rateQuantity = nil
end
when FHIR::STU3::MedicationDispense
- resource.medicationReference = textonly_reference('Medication', FHIR::STU3)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::STU3)
resource.medicationCodeableConcept = nil
resource.dosageInstruction.each {|d|d.timing = nil }
resource.quantity.comparator = nil unless resource.quantity.nil?
resource.daysSupply.comparator = nil unless resource.daysSupply.nil?
when FHIR::STU3::MedicationRequest
- resource.medicationReference = textonly_reference('Medication', FHIR::STU3)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::STU3)
resource.medicationCodeableConcept = nil
resource.dosageInstruction.each {|d|d.timing = nil }
resource.dispenseRequest.quantity.comparator = nil if resource&.dispenseRequest&.quantity != nil
when FHIR::STU3::MedicationStatement
resource.reasonNotTaken = nil unless resource.taken == 'n'
- resource.medicationReference = textonly_reference('Medication', FHIR::STU3)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::STU3)
resource.medicationCodeableConcept = nil
resource.dosage.each{|d|d.timing=nil}
when FHIR::STU3::MessageDefinition
@@ -1503,7 +1561,7 @@ def self.apply_invariants!(resource)
p.searchType = nil unless p.type == 'string'
end
when FHIR::STU3::Patient
- resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S', FHIR::STU3)
+ resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S', namespace: FHIR::STU3)
when FHIR::STU3::PlanDefinition
resource.action.each do |a|
a.action.each do |b|
@@ -1514,7 +1572,7 @@ def self.apply_invariants!(resource)
resource.notDoneReason = nil if resource.notDone != true
resource.focalDevice.each do |fd|
code = ['implanted', 'explanted', 'manipulated'].sample
- fd.action = minimal_codeableconcept('http://hl7.org/fhir/device-action', code, FHIR::STU3)
+ fd.action = minimal_codeableconcept('http://hl7.org/fhir/device-action', code, namespace: FHIR::STU3)
end
when FHIR::STU3::Provenance
resource.entity.each do |e|
@@ -1528,7 +1586,7 @@ def self.apply_invariants!(resource)
end
end
when FHIR::STU3::RelatedPerson
- resource.relationship = minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','family', FHIR::STU3)
+ resource.relationship = minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','family', namespace: FHIR::STU3)
when FHIR::STU3::Questionnaire
resource.item.each do |i|
i.required = true
@@ -1583,7 +1641,7 @@ def self.apply_invariants!(resource)
when FHIR::STU3::SampledData
resource.origin.comparator = nil unless resource.origin.nil?
when FHIR::STU3::Signature
- resource.type = [ minimal_coding('urn:iso-astm:E1762-95:2013','1.2.840.10065.1.12.1.18', FHIR::STU3) ]
+ resource.type = [ minimal_coding('urn:iso-astm:E1762-95:2013','1.2.840.10065.1.12.1.18', namespace: FHIR::STU3) ]
resource.whoUri = 'http://projectcrucible.org'
resource.whoReference = nil
when FHIR::STU3::Specimen
@@ -1603,10 +1661,10 @@ def self.apply_invariants!(resource)
instance.quantity.comparator = nil unless instance.quantity.nil?
end
when FHIR::STU3::SupplyDelivery
- resource.type = minimal_codeableconcept('http://hl7.org/fhir/supply-item-type','medication', FHIR::STU3)
+ resource.type = minimal_codeableconcept('http://hl7.org/fhir/supply-item-type','medication', namespace: FHIR::STU3)
resource.suppliedItem.quantity.comparator = nil if !resource.suppliedItem.nil? && !resource.suppliedItem.quantity.nil?
when FHIR::STU3::SupplyRequest
- resource.category = minimal_codeableconcept('http://hl7.org/fhir/supply-kind','central', FHIR::STU3)
+ resource.category = minimal_codeableconcept('http://hl7.org/fhir/supply-kind','central', namespace: FHIR::STU3)
when FHIR::STU3::StructureDefinition
resource.derivation = 'constraint'
resource.fhirVersion = 'STU3'
@@ -1765,10 +1823,10 @@ def self.apply_invariants!(resource)
# DSTU2
when FHIR::DSTU2::Appointment
- resource.reason = nil # minimal_codeableconcept('http://snomed.info/sct','219006', FHIR::DSTU2) # drinker of alcohol
- resource.participant.each{|p| p.type=[ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', FHIR::DSTU2) ] }
+ resource.reason = nil # minimal_codeableconcept('http://snomed.info/sct','219006', namespace: FHIR::DSTU2) # drinker of alcohol
+ resource.participant.each{|p| p.type=[ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', namespace: FHIR::DSTU2) ] }
when FHIR::DSTU2::AppointmentResponse
- resource.participantType = [ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', FHIR::DSTU2) ]
+ resource.participantType = [ minimal_codeableconcept('http://hl7.org/fhir/participant-type','emergency', namespace: FHIR::DSTU2) ]
when FHIR::DSTU2::AuditEvent
resource.object.each do |o|
o.query=nil
@@ -1808,35 +1866,35 @@ def self.apply_invariants!(resource)
end
when FHIR::DSTU2::Claim
resource.item.each do |item|
- item.type = minimal_coding('http://hl7.org/fhir/v3/ActCode','OHSINV', FHIR::DSTU2)
+ item.type = minimal_coding('http://hl7.org/fhir/v3/ActCode','OHSINV', namespace: FHIR::DSTU2)
item.quantity.comparator = nil unless item.quantity.nil?
item.detail.each do |detail|
- detail.type = minimal_coding('http://hl7.org/fhir/v3/ActCode','OHSINV', FHIR::DSTU2)
+ detail.type = minimal_coding('http://hl7.org/fhir/v3/ActCode','OHSINV', namespace: FHIR::DSTU2)
detail.quantity.comparator = nil unless detail.quantity.nil?
detail.subDetail.each do |sub|
- sub.type = minimal_coding('http://hl7.org/fhir/v3/ActCode','OHSINV', FHIR::DSTU2)
- sub.service = minimal_coding('http://hl7.org/fhir/ex-USCLS','1205', FHIR::DSTU2)
+ sub.type = minimal_coding('http://hl7.org/fhir/v3/ActCode','OHSINV', namespace: FHIR::DSTU2)
+ sub.service = minimal_coding('http://hl7.org/fhir/ex-USCLS','1205', namespace: FHIR::DSTU2)
sub.quantity.comparator = nil unless sub.quantity.nil?
end
end
end
resource.missingTeeth.each do |mt|
- mt.tooth = minimal_coding('http://hl7.org/fhir/ex-fdi','42', FHIR::DSTU2)
+ mt.tooth = minimal_coding('http://hl7.org/fhir/ex-fdi','42', namespace: FHIR::DSTU2)
end
when FHIR::DSTU2::ClaimResponse
resource.item.each do |item|
- item.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', FHIR::DSTU2)}
+ item.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::DSTU2)}
item.detail.each do |detail|
- detail.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', FHIR::DSTU2)}
+ detail.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::DSTU2)}
detail.subDetail.each do |sub|
- sub.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', FHIR::DSTU2)}
+ sub.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::DSTU2)}
end
end
end
resource.addItem.each do |addItem|
- addItem.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', FHIR::DSTU2)}
+ addItem.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::DSTU2)}
addItem.detail.each do |detail|
- detail.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', FHIR::DSTU2)}
+ detail.adjudication.each{|a|a.code = minimal_coding('http://hl7.org/fhir/adjudication','benefit', namespace: FHIR::DSTU2)}
end
end
when FHIR::DSTU2::Communication
@@ -1854,10 +1912,10 @@ def self.apply_invariants!(resource)
end
when FHIR::DSTU2::ConceptMap
if(resource.sourceUri.nil? && resource.sourceReference.nil?)
- resource.sourceReference = textonly_reference('ValueSet', FHIR::DSTU2)
+ resource.sourceReference = textonly_reference('ValueSet', namespace: FHIR::DSTU2)
end
if(resource.targetUri.nil? && resource.targetReference.nil?)
- resource.targetReference = textonly_reference('ValueSet', FHIR::DSTU2)
+ resource.targetReference = textonly_reference('ValueSet', namespace: FHIR::DSTU2)
end
when FHIR::DSTU2::Conformance
resource.fhirVersion = 'DSTU2'
@@ -1871,18 +1929,18 @@ def self.apply_invariants!(resource)
resource.messaging.each{|m| m.endpoint = nil} if resource.kind != 'instance'
when FHIR::DSTU2::Contract
resource.actor.each do |actor|
- actor.entity = textonly_reference('Patient', FHIR::DSTU2)
+ actor.entity = textonly_reference('Patient', namespace: FHIR::DSTU2)
end
resource.valuedItem.each do |valuedItem|
valuedItem.quantity.comparator = nil unless valuedItem.quantity.nil?
end
resource.term.each do |term|
term.actor.each do |actor|
- actor.entity = textonly_reference('Organization', FHIR::DSTU2)
+ actor.entity = textonly_reference('Organization', namespace: FHIR::DSTU2)
end
term.group.each do |group|
group.actor.each do |actor|
- actor.entity = textonly_reference('Organization', FHIR::DSTU2)
+ actor.entity = textonly_reference('Organization', namespace: FHIR::DSTU2)
end
end
term.valuedItem.each do |valuedItem|
@@ -1891,15 +1949,15 @@ def self.apply_invariants!(resource)
end
resource.friendly.each do |f|
f.contentAttachment = nil
- f.contentReference = textonly_reference('DocumentReference', FHIR::DSTU2)
+ f.contentReference = textonly_reference('DocumentReference', namespace: FHIR::DSTU2)
end
resource.legal.each do |f|
f.contentAttachment = nil
- f.contentReference = textonly_reference('DocumentReference', FHIR::DSTU2)
+ f.contentReference = textonly_reference('DocumentReference', namespace: FHIR::DSTU2)
end
resource.rule.each do |f|
f.contentAttachment = nil
- f.contentReference = textonly_reference('DocumentReference', FHIR::DSTU2)
+ f.contentReference = textonly_reference('DocumentReference', namespace: FHIR::DSTU2)
end
when FHIR::DSTU2::DataElement
resource.mapping.each do |m|
@@ -1922,10 +1980,10 @@ def self.apply_invariants!(resource)
when FHIR::DSTU2::DocumentManifest
resource.content.each do |c|
c.pAttachment = nil
- c.pReference = textonly_reference('Any', FHIR::DSTU2)
+ c.pReference = textonly_reference('Any', namespace: FHIR::DSTU2)
end
when FHIR::DSTU2::DocumentReference
- resource.docStatus = minimal_codeableconcept('http://hl7.org/fhir/composition-status','preliminary', FHIR::DSTU2)
+ resource.docStatus = minimal_codeableconcept('http://hl7.org/fhir/composition-status','preliminary', namespace: FHIR::DSTU2)
when FHIR::DSTU2::ElementDefinition
keys = []
resource.constraint.each do |constraint|
@@ -1949,7 +2007,7 @@ def self.apply_invariants!(resource)
when FHIR::DSTU2::Goal
resource.outcome.each do |outcome|
outcome.resultCodeableConcept = nil
- outcome.resultReference = textonly_reference('Observation', FHIR::DSTU2)
+ outcome.resultReference = textonly_reference('Observation', namespace: FHIR::DSTU2)
end
when FHIR::DSTU2::Group
resource.member = [] if resource.actual==false
@@ -1974,7 +2032,7 @@ def self.apply_invariants!(resource)
# resource.uid = random_oid
# index = SecureRandom.random_number(FHIR::DSTU2::ImagingObjectSelection::VALID_CODES[:title].length)
# code = FHIR::DSTU2::ImagingObjectSelection::VALID_CODES[:title][index]
- # resource.title = minimal_codeableconcept('http://nema.org/dicom/dicm',code, FHIR::DSTU2)
+ # resource.title = minimal_codeableconcept('http://nema.org/dicom/dicm',code, namespace: FHIR::DSTU2)
# resource.study.each do |study|
# study.uid = random_oid
# study.series.each do |series|
@@ -1997,13 +2055,13 @@ def self.apply_invariants!(resource)
when FHIR::DSTU2::Immunization
if resource.wasNotGiven
resource.explanation = FHIR::DSTU2::Immunization::Explanation.new unless resource.explanation
- resource.explanation.reasonNotGiven = [ textonly_codeableconcept("reasonNotGiven #{SecureRandom.base64}", FHIR::DSTU2) ]
+ resource.explanation.reasonNotGiven = [ textonly_codeableconcept("reasonNotGiven #{SecureRandom.base64}", namespace: FHIR::DSTU2) ]
resource.explanation.reason = nil
resource.reaction = nil
else
resource.explanation = FHIR::DSTU2::Immunization::Explanation.new unless resource.explanation
resource.explanation.reasonNotGiven = nil
- resource.explanation.reason = [ textonly_codeableconcept("reason #{SecureRandom.base64}", FHIR::DSTU2) ]
+ resource.explanation.reason = [ textonly_codeableconcept("reason #{SecureRandom.base64}", namespace: FHIR::DSTU2) ]
end
resource.doseQuantity.comparator = nil unless resource.doseQuantity.nil?
when FHIR::DSTU2::ImplementationGuide
@@ -2011,7 +2069,7 @@ def self.apply_invariants!(resource)
resource.package.each do |package|
package.resource.each do |r|
r.sourceUri = nil
- r.sourceReference = textonly_reference('Any', FHIR::DSTU2)
+ r.sourceReference = textonly_reference('Any', namespace: FHIR::DSTU2)
end
end
when FHIR::DSTU2::List
@@ -2047,20 +2105,20 @@ def self.apply_invariants!(resource)
else
resource.reasonNotGiven = nil
end
- resource.medicationReference = textonly_reference('Medication', FHIR::DSTU2)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::DSTU2)
resource.medicationCodeableConcept = nil
resource.dosage.quantity.comparator = nil if !resource&.dosage&.quantity.nil?
when FHIR::DSTU2::MedicationDispense
- resource.medicationReference = textonly_reference('Medication', FHIR::DSTU2)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::DSTU2)
resource.medicationCodeableConcept = nil
resource.dosageInstruction.each {|d|d.timing = nil }
when FHIR::DSTU2::MedicationOrder
- resource.medicationReference = textonly_reference('Medication', FHIR::DSTU2)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::DSTU2)
resource.medicationCodeableConcept = nil
resource.dosageInstruction.each {|d|d.timing = nil }
when FHIR::DSTU2::MedicationStatement
resource.reasonNotTaken = nil if resource.wasNotTaken != true
- resource.medicationReference = textonly_reference('Medication', FHIR::DSTU2)
+ resource.medicationReference = textonly_reference('Medication', namespace: FHIR::DSTU2)
resource.medicationCodeableConcept = nil
resource.dosage.each do |d|
d.timing=nil
@@ -2107,7 +2165,7 @@ def self.apply_invariants!(resource)
when FHIR::DSTU2::Order
resource.when.schedule = nil
when FHIR::DSTU2::Patient
- resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S', FHIR::DSTU2)
+ resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S', namespace: FHIR::DSTU2)
resource.communication.each do |communication|
communication.language.coding.each do |c|
c.system = 'http://tools.ietf.org/html/bcp47'
@@ -2117,7 +2175,7 @@ def self.apply_invariants!(resource)
when FHIR::DSTU2::Procedure
resource.reasonNotPerformed = nil if resource.notPerformed != true
resource.focalDevice.each do |fd|
- fd.action = minimal_codeableconcept('http://hl7.org/fhir/ValueSet/device-action','implanted', FHIR::DSTU2)
+ fd.action = minimal_codeableconcept('http://hl7.org/fhir/ValueSet/device-action','implanted', namespace: FHIR::DSTU2)
end
when FHIR::DSTU2::Provenance
resource.entity.each do |e|
@@ -2131,7 +2189,7 @@ def self.apply_invariants!(resource)
end
end
when FHIR::DSTU2::RelatedPerson
- resource.relationship = minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','family', FHIR::DSTU2)
+ resource.relationship = minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','family', namespace: FHIR::DSTU2)
when FHIR::DSTU2::Questionnaire
resource.group.required = true
resource.group.group = nil
@@ -2144,13 +2202,13 @@ def self.apply_invariants!(resource)
resource.channel.payload = 'applicaton/json+fhir'
resource.end = nil
when FHIR::DSTU2::SupplyDelivery
- resource.type = minimal_codeableconcept('http://hl7.org/fhir/supply-item-type','medication', FHIR::DSTU2)
+ resource.type = minimal_codeableconcept('http://hl7.org/fhir/supply-item-type','medication', namespace: FHIR::DSTU2)
resource.quantity.comparator = nil unless resource.quantity.nil?
when FHIR::DSTU2::SupplyRequest
- resource.kind = minimal_codeableconcept('http://hl7.org/fhir/supply-kind','central', FHIR::DSTU2)
+ resource.kind = minimal_codeableconcept('http://hl7.org/fhir/supply-kind','central', namespace: FHIR::DSTU2)
if resource.when
resource.when.schedule = nil
- resource.when.code = minimal_codeableconcept('http://snomed.info/sct','20050000', FHIR::DSTU2) #biweekly
+ resource.when.code = minimal_codeableconcept('http://snomed.info/sct','20050000', namespace: FHIR::DSTU2) #biweekly
end
when FHIR::DSTU2::StructureDefinition
resource.fhirVersion = 'DSTU2'
diff --git a/lib/tasks/fhir_structure.rake b/lib/tasks/fhir_structure.rake
new file mode 100644
index 00000000..c5c9e7ce
--- /dev/null
+++ b/lib/tasks/fhir_structure.rake
@@ -0,0 +1,15 @@
+namespace :crucible do
+ desc 'Generate the R4B FHIR structure index from the official definitions archive'
+ task :generate_r4b_structure, [:definitions_archive] do |_task, args|
+ unless args.definitions_archive
+ raise 'Usage: rake "crucible:generate_r4b_structure[path/to/r4b-definitions.json.zip]"'
+ end
+
+ root = File.expand_path('../..', __dir__)
+ Crucible::FHIRStructureGenerator.write_from_archive(
+ File.expand_path(args.definitions_archive),
+ File.join(root, 'lib', 'FHIR_structure_r4.json'),
+ File.join(root, 'lib', 'FHIR_structure_r4b.json')
+ )
+ end
+end
diff --git a/lib/tasks/tasks.rake b/lib/tasks/tasks.rake
index f8fb0be6..d48c20f5 100644
--- a/lib/tasks/tasks.rake
+++ b/lib/tasks/tasks.rake
@@ -36,8 +36,7 @@ namespace :crucible do
require 'benchmark'
result = {}
b = Benchmark.measure {
- client = FHIR::Client.new(args.url)
- client.use_fhir_version(fhir_version)
+ client = FHIR::Client.new(args.url, fhir_version: fhir_version)
client.setup_security
result = execute_all(args.url, client, args.output)
}
@@ -47,11 +46,12 @@ namespace :crucible do
end
desc 'execute all test scripts'
- task :execute_all_testscripts, [:url, :output] do |t, args|
+ task :execute_all_testscripts, [:url, :fhir_version, :output] do |t, args|
FHIR.logger = Logger.new("logs/plan_executor.log", 10, 1024000)
+ fhir_version = resolve_testscript_fhir_version(args.fhir_version)
require 'benchmark'
b = Benchmark.measure {
- client = FHIR::Client.new(args.url)
+ client = FHIR::Client.new(args.url, fhir_version: fhir_version)
client.setup_security
results = Crucible::Tests::TestScriptEngine.new(client).execute_all
process_results(results, args.url, args.output)
@@ -60,11 +60,12 @@ namespace :crucible do
end
desc 'execute testscript and get testreport'
- task :testreport, [:url, :test, :filename] do |t, args|
+ task :testreport, [:url, :fhir_version, :test, :filename] do |t, args|
FHIR.logger = Logger.new("logs/plan_executor.log", 10, 1024000)
+ fhir_version = resolve_testscript_fhir_version(args.fhir_version)
require 'benchmark'
b = Benchmark.measure {
- client = FHIR::Client.new(args.url)
+ client = FHIR::Client.new(args.url, fhir_version: fhir_version)
client.setup_security
engine = Crucible::Tests::TestScriptEngine.new(client)
script = engine.find_test(args.test)
@@ -90,8 +91,7 @@ namespace :crucible do
require 'benchmark'
result = {}
b = Benchmark.measure {
- client = FHIR::Client.new(args.url)
- client.use_fhir_version(fhir_version)
+ client = FHIR::Client.new(args.url, fhir_version: fhir_version)
client.setup_security
result = execute_test(args.url, client, args.test, args.resource, args.output)
}
@@ -144,10 +144,15 @@ namespace :crucible do
end
def resolve_fhir_version(version_string)
- fhir_version = :r4
- fhir_version = :stu3 if version_string.to_s.downcase == 'stu3'
- fhir_version = :dstu2 if version_string.to_s.downcase == 'dstu2'
- fhir_version
+ Crucible::FHIRVersion.resolve(version_string)
+ end
+
+ def resolve_testscript_fhir_version(version_string)
+ version = resolve_fhir_version(version_string)
+ return version if version == :stu3
+
+ raise Crucible::FHIRVersion::UnsupportedVersionError,
+ "FHIR TestScripts require STU3, got #{version}"
end
def execute_test(url, client, key, resourceType=nil, output=nil)
@@ -350,8 +355,7 @@ namespace :crucible do
puts "## #{url}"
puts "```"
b = Benchmark.measure {
- client = FHIR::Client.new(url)
- client.use_fhir_version(fhir_version)
+ client = FHIR::Client.new(url, fhir_version: fhir_version)
client.setup_security
execute_test(url, client, args.test, args.resource_type, args.output)
}
@@ -377,8 +381,7 @@ namespace :crucible do
puts "## #{url}"
puts "```"
b = Benchmark.measure {
- client = FHIR::Client.new(url)
- client.use_fhir_version(fhir_version)
+ client = FHIR::Client.new(url, fhir_version: fhir_version)
client.setup_security
results = execute_all(url, client, output)
}
@@ -453,8 +456,7 @@ namespace :crucible do
end
end
- client = FHIR::Client.new(args.url)
- client.use_fhir_version(fhir_version)
+ client = FHIR::Client.new(args.url, fhir_version: fhir_version)
client.setup_security
client.monitor_requirements
test = args.test.to_sym
diff --git a/lib/tests/base_test.rb b/lib/tests/base_test.rb
index 2954a5cd..dd905d06 100644
--- a/lib/tests/base_test.rb
+++ b/lib/tests/base_test.rb
@@ -38,11 +38,12 @@ def initialize(client, client2=nil)
FHIR::Resource.new.client = client
FHIR::DSTU2::Resource.new.client = client
FHIR::STU3::Resource.new.client = client
+ FHIR::R4B::Resource.new.client = client
@client2 = client2
@client.monitor_requests if @client
@client2.monitor_requests if @client2
@tags ||= []
- @supported_versions ||= [:dstu2, :stu3, :r4]
+ @supported_versions ||= []
@warnings = []
@setup_failed = false
@setup_requests = []
@@ -50,13 +51,9 @@ def initialize(client, client2=nil)
end
def version_namespace
- if @client&.fhir_version.to_s.upcase == 'DSTU2'
- "FHIR::DSTU2".constantize
- elsif @client&.fhir_version.to_s.upcase == 'STU3'
- "FHIR::STU3".constantize
- else
- "FHIR".constantize
- end
+ raise ArgumentError, 'A versioned FHIR client is required' unless @client
+
+ Crucible::FHIRVersion.namespace(@client.fhir_version)
end
def multiserver
diff --git a/lib/tests/suites/base_suite.rb b/lib/tests/suites/base_suite.rb
index 60071fb9..5cc4cb7c 100644
--- a/lib/tests/suites/base_suite.rb
+++ b/lib/tests/suites/base_suite.rb
@@ -12,8 +12,8 @@ def parse_operation_outcome(body)
# body should be a String
outcome = nil
begin
- outcome = FHIR.from_contents(body)
- outcome = nil if outcome.class!=FHIR::OperationOutcome
+ outcome = version_namespace.from_contents(body)
+ outcome = nil unless outcome.is_a?(version_namespace.const_get(:OperationOutcome))
rescue
outcome = nil
end
@@ -32,11 +32,9 @@ def build_messages(operation_outcome)
# move to another area?
# also, this may be causing a problem on the fhir starburst structure
def fhir_version
- if @client.nil?
- :r4
- else
- @client.fhir_version
- end
+ raise ArgumentError, 'A versioned FHIR client is required' unless @client
+
+ @client.fhir_version
end
def get_resource(resource)
@@ -48,45 +46,23 @@ def fhir_resources
end
def resource_from_contents(body)
- if @client.fhir_version.to_s.upcase == 'DSTU2'
- FHIR::DSTU2.from_contents(body)
- else
- FHIR.from_contents(body)
- end
+ version_namespace.from_contents(body)
end
def self.get_resource(fhir_version, resource)
- if fhir_version.to_s.upcase == 'DSTU2'
- "FHIR::DSTU2::#{resource}".constantize
- elsif fhir_version.to_s.upcase == 'STU3'
- "FHIR::STU3::#{resource}".constantize
- else
- "FHIR::#{resource}".constantize
- end
-
+ Crucible::FHIRVersion.namespace(fhir_version).const_get(resource)
end
def self.valid_resource?(fhir_version, resource)
- if fhir_version.to_s.upcase == 'DSTU2'
- FHIR::DSTU2::RESOURCES.include?(resource)
- elsif fhir_version.to_s.upcase == 'STU3'
- FHIR::STU3::RESOURCES.include?(resource)
- else
- FHIR::RESOURCES.include?(resource)
- end
+ Crucible::FHIRVersion.namespace(fhir_version).const_get(:RESOURCES).include?(resource.to_s)
end
- def self.fhir_resources(fhir_version=nil)
-
- resources = FHIR::RESOURCES
- namespace = 'FHIR'
- if !fhir_version.nil? && FHIR.constants.include?(fhir_version.upcase)
- resources = FHIR.const_get(fhir_version.upcase)::RESOURCES
- namespace = "FHIR::#{fhir_version.to_s.upcase}"
- end
-
- resources.select {|r| !EXCLUDED_RESOURCES.include?(r)}.map {|r| "#{namespace}::#{r}".constantize}
+ def self.fhir_resources(fhir_version)
+ namespace = Crucible::FHIRVersion.namespace(fhir_version)
+ namespace.const_get(:RESOURCES)
+ .reject { |resource| EXCLUDED_RESOURCES.include?(resource) }
+ .map { |resource| namespace.const_get(resource) }
end
def requires(hash)
@@ -163,12 +139,7 @@ def self.test(key, desc, &block)
def resource_category(resource)
unless @resource_category
@categories_by_resource = {}
- fhir_version = :r4
- if resource.name.start_with? 'FHIR::DSTU2'
- fhir_version = :dstu2
- elsif resource.name.start_with? 'FHIR::STU3'
- fhir_version = :stu3
- end
+ fhir_version = Crucible::FHIRVersion.for_class(resource)
fhir_structure = Crucible::FHIRStructure.get(fhir_version)
categories = fhir_structure['children'].select {|n| n['name'] == 'RESOURCES'}.first['children']
pull_children = lambda {|n, chain| n['children'].nil? ? n['name'] : n['children'].map {|child| chain.call(child, chain)}}
diff --git a/lib/tests/suites/connectathon_terminology_track.rb b/lib/tests/suites/connectathon_terminology_track.rb
index 100f22b0..c7d2cd35 100644
--- a/lib/tests/suites/connectathon_terminology_track.rb
+++ b/lib/tests/suites/connectathon_terminology_track.rb
@@ -212,7 +212,7 @@ def teardown
validates resource: 'ValueSet', methods: ['create']
}
- @resources = Crucible::Generator::Resources.new
+ @resources = Crucible::Generator::Resources.new(fhir_version)
@codesystem_simple = @resources.codesystem_simple
@valueset_simple = @resources.valueset_simple
@@ -325,7 +325,7 @@ def teardown
validates resource: 'ConceptMap', methods: ['create']
}
- @resources = Crucible::Generator::Resources.new
+ @resources = Crucible::Generator::Resources.new(fhir_version)
@conceptmap_simple = @resources.conceptmap_simple
@conceptmap_simple.id = nil
@conceptmap_simple.url = @conceptmap_simple.url + rand(10000000).to_s
diff --git a/lib/tests/suites/fhir_path_patch_test.rb b/lib/tests/suites/fhir_path_patch_test.rb
index 9fa956e0..cf5ba165 100644
--- a/lib/tests/suites/fhir_path_patch_test.rb
+++ b/lib/tests/suites/fhir_path_patch_test.rb
@@ -14,7 +14,7 @@ def initialize(client1, client2 = nil)
super(client1, client2)
@tags.append('fhirpath')
@category = { id: 'fhirpath', title: 'FHIRPath' }
- @supported_versions = [:stu3, :r4]
+ @supported_versions = [:stu3, :r4, :r4b]
end
def setup
@@ -29,7 +29,7 @@ def setup
end
def teardown
- @client.destroy(FHIR::MedicationRequest, @medication_order_id) unless @medication_order_id.nil?
+ @client.destroy(get_resource(:MedicationRequest), @medication_order_id) unless @medication_order_id.nil?
end
['JSON', 'XML'].each do |fmt|
@@ -47,9 +47,10 @@ def teardown
validates resource: 'MedicationRequest', methods: ['read']
}
- reply = @client.read(FHIR::MedicationRequest, @medication_order_id, resource_format(fmt))
+ medication_request = get_resource(:MedicationRequest)
+ reply = @client.read(medication_request, @medication_order_id, resource_format(fmt))
assert_response_ok(reply)
- assert_resource_type(reply, FHIR::MedicationRequest)
+ assert_resource_type(reply, medication_request)
assert_resource_content_type(reply, fmt.downcase)
warning {
assert(!reply.resource.meta.nil?, 'Last Updated and VersionId not present.')
@@ -73,14 +74,15 @@ def teardown
format = resource_format(fmt)
patchset = patchset_resource("replace", "MedicationRequest.status", nil, "completed")
- reply = @client.fhir_patch(FHIR::MedicationRequest, @medication_order_id, patchset, {}, format)
+ medication_request = get_resource(:MedicationRequest)
+ reply = @client.fhir_patch(medication_request, @medication_order_id, patchset, {}, format)
assert_response_ok(reply)
warning {
- assert_resource_type(reply, FHIR::MedicationRequest)
+ assert_resource_type(reply, medication_request)
assert_resource_content_type(reply, fmt.downcase)
}
- reply = @client.read(FHIR::MedicationRequest, @medication_order_id, format)
+ reply = @client.read(medication_request, @medication_order_id, format)
assert_response_ok(reply)
assert_equal(reply.resource.status, 'completed', 'Status not updated from patch.')
warning {
@@ -109,10 +111,11 @@ def teardown
# According to the FHIR spec, the If-Match eTag for version id should be weak.
additional_headers = { 'If-Match' => "\"#{@previous_version_id}\"" }
- reply = @client.fhir_patch(FHIR::MedicationRequest, @medication_order_id, patchset, {}, resource_format(fmt), additional_headers)
+ medication_request = get_resource(:MedicationRequest)
+ reply = @client.fhir_patch(medication_request, @medication_order_id, patchset, {}, resource_format(fmt), additional_headers)
assert_response_conflict(reply)
- reply = @client.read(FHIR::MedicationRequest, @medication_order_id, resource_format(fmt))
+ reply = @client.read(medication_request, @medication_order_id, resource_format(fmt))
assert_response_ok(reply)
assert_equal(reply.resource.status, 'completed', 'Resource should not have been patched because version id was stale.')
diff --git a/lib/tests/suites/format_test.rb b/lib/tests/suites/format_test.rb
index 8e903aa0..1cf27605 100644
--- a/lib/tests/suites/format_test.rb
+++ b/lib/tests/suites/format_test.rb
@@ -16,7 +16,8 @@ def description
def initialize(client1, client2=nil)
super(client1, client2)
- if fhir_version == :dstu2
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
+ if client1&.fhir_version == :dstu2
@xml_format = FHIR::Formats::ResourceFormat::RESOURCE_XML_DSTU2
@json_format = FHIR::Formats::ResourceFormat::RESOURCE_JSON_DSTU2
else
diff --git a/lib/tests/suites/history_test.rb b/lib/tests/suites/history_test.rb
index 103f117d..b94c3cd1 100644
--- a/lib/tests/suites/history_test.rb
+++ b/lib/tests/suites/history_test.rb
@@ -12,6 +12,7 @@ def description
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
@category = {id: 'core_functionality', title: 'Core Functionality'}
end
diff --git a/lib/tests/suites/incendi_consent_search_by_patient_reference_329.rb b/lib/tests/suites/incendi_consent_search_by_patient_reference_329.rb
index 11395657..e741c523 100644
--- a/lib/tests/suites/incendi_consent_search_by_patient_reference_329.rb
+++ b/lib/tests/suites/incendi_consent_search_by_patient_reference_329.rb
@@ -14,12 +14,12 @@ def initialize(client1, client2 = nil)
super(client1, client2)
@tags.append('indendilabs')
@category = { id: 'indendilabs', title: 'Indendilabs' }
- @supported_versions = [:stu3, :r4]
+ @supported_versions = [:stu3, :r4, :r4b]
end
def setup
- @patient = ResourceGenerator.minimal_patient(nil, nil, version_namespace)
+ @patient = ResourceGenerator.minimal_patient(nil, nil, namespace: version_namespace)
reply = @client.create(@patient)
assert_response_ok(reply)
@patient_id = reply.id
diff --git a/lib/tests/suites/incendi_elements_search_parameter.rb b/lib/tests/suites/incendi_elements_search_parameter.rb
index 15416b9b..cf412da5 100644
--- a/lib/tests/suites/incendi_elements_search_parameter.rb
+++ b/lib/tests/suites/incendi_elements_search_parameter.rb
@@ -14,11 +14,11 @@ def initialize(client1, client2 = nil)
super(client1, client2)
@tags.append('incendilabs')
@category = { id: 'incendilabs', title: 'Incendilabs' }
- @supported_versions = [:stu3, :r4]
+ @supported_versions = [:stu3, :r4, :r4b]
end
def setup
- @patient = ResourceGenerator.minimal_patient('elements-search-parameter', 'Elements', version_namespace)
+ @patient = ResourceGenerator.minimal_patient('elements-search-parameter', 'Elements', namespace: version_namespace)
@patient.gender = 'male'
@patient.birthDate = '1974-12-25'
diff --git a/lib/tests/suites/incendi_unknown_search_parameter_1160.rb b/lib/tests/suites/incendi_unknown_search_parameter_1160.rb
index 4612055e..c8398f54 100644
--- a/lib/tests/suites/incendi_unknown_search_parameter_1160.rb
+++ b/lib/tests/suites/incendi_unknown_search_parameter_1160.rb
@@ -14,7 +14,7 @@ def initialize(client1, client2 = nil)
super(client1, client2)
@tags.append('incendilabs')
@category = { id: 'incendilabs', title: 'Incendilabs' }
- @supported_versions = [:stu3, :r4]
+ @supported_versions = [:stu3, :r4, :r4b]
end
def setup
diff --git a/lib/tests/suites/read_test.rb b/lib/tests/suites/read_test.rb
index c6f5f942..a44c6900 100644
--- a/lib/tests/suites/read_test.rb
+++ b/lib/tests/suites/read_test.rb
@@ -12,6 +12,7 @@ def description
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
@category = {id: 'core_functionality', title: 'Core Functionality'}
end
diff --git a/lib/tests/suites/resource_test.rb b/lib/tests/suites/resource_test.rb
index 545da4f2..ef33ac98 100644
--- a/lib/tests/suites/resource_test.rb
+++ b/lib/tests/suites/resource_test.rb
@@ -50,6 +50,7 @@ def category
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
end
# this allows results to have unique ids for resource based tests
diff --git a/lib/tests/suites/search_test.rb b/lib/tests/suites/search_test.rb
index 531c2395..007397fb 100644
--- a/lib/tests/suites/search_test.rb
+++ b/lib/tests/suites/search_test.rb
@@ -38,6 +38,7 @@ def category
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
end
# this allows results to have unique ids for resource based tests
diff --git a/lib/tests/suites/search_test_robust.rb b/lib/tests/suites/search_test_robust.rb
index cee555e3..9ff5c862 100644
--- a/lib/tests/suites/search_test_robust.rb
+++ b/lib/tests/suites/search_test_robust.rb
@@ -13,7 +13,7 @@ def description
def initialize(client1, client2=nil)
super(client1, client2)
@category = {id: 'core_functionality', title: 'Core Functionality'}
- @supported_versions = [:stu3, :r4]
+ @supported_versions = [:stu3, :r4, :r4b]
end
def setup
@@ -39,7 +39,7 @@ def teardown
skip 'TODO: https://github.com/FirelyTeam/spark/issues/310'
- match_patient = Crucible::Generator::Resources.new.minimal_patient
+ match_patient = Crucible::Generator::Resources.new(fhir_version).minimal_patient
match_patient.identifier = nil
reply = @client.match(match_patient)
assert_response_ok(reply)
diff --git a/lib/tests/suites/sprinkler_search_test.rb b/lib/tests/suites/sprinkler_search_test.rb
index f729aeea..28a8dce4 100644
--- a/lib/tests/suites/sprinkler_search_test.rb
+++ b/lib/tests/suites/sprinkler_search_test.rb
@@ -14,6 +14,7 @@ def description
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
@category = {id: 'core_functionality', title: 'Core Functionality'}
end
diff --git a/lib/tests/suites/suite_engine.rb b/lib/tests/suites/suite_engine.rb
index e58dd027..72927c02 100644
--- a/lib/tests/suites/suite_engine.rb
+++ b/lib/tests/suites/suite_engine.rb
@@ -27,7 +27,7 @@ def self.list_all(metadata=false)
test_class = test.class.name.demodulize
#if t can set class
if test.respond_to? 'resource_class='
- [:dstu2, :stu3, :r4].each do |fhir_version|
+ (Crucible::FHIRVersion::KNOWN & test.supported_versions).each do |fhir_version|
Crucible::Tests::BaseSuite.fhir_resources(fhir_version).each do |klass|
klass_name = klass.name.demodulize
test_name = "#{test_class}#{klass_name}"
@@ -81,7 +81,8 @@ def build_suites_map
end
end
- def self.generate_metadata
+ def self.generate_metadata(fhir_version)
+ version = Crucible::FHIRVersion.resolve(fhir_version)
metadata = {}
puts "---"
puts "BUILDING METADATA"
@@ -89,8 +90,8 @@ def self.generate_metadata
SuiteEngine.new.tests.each do |test|
test_file = Crucible::Tests.const_get(test).new(nil)
if test_file.respond_to? 'resource_class='
- Crucible::Tests::BaseSuite.fhir_resources.each do |klass|
- test_file.resource_class = Module.const_get("FHIR::#{klass}")
+ Crucible::Tests::BaseSuite.fhir_resources(version).each do |klass|
+ test_file.resource_class = klass
puts "---"
puts "BUILDING METADATA - #{test}#{klass}"
puts "---"
diff --git a/lib/tests/suites/transaction_test.rb b/lib/tests/suites/transaction_test.rb
index c78e9d19..61c98886 100644
--- a/lib/tests/suites/transaction_test.rb
+++ b/lib/tests/suites/transaction_test.rb
@@ -12,6 +12,7 @@ def description
def initialize(client1, client2=nil)
super(client1, client2)
+ @supported_versions = [:dstu2, :stu3, :r4, :r4b]
@category = {id: 'core_functionality', title: 'Core Functionality'}
end
@@ -63,18 +64,24 @@ def teardown
validates resource: nil, methods: ['transaction-system']
}
- @patient0 = ResourceGenerator.minimal_patient("#{Time.now.to_i}",'Transaction', version_namespace)
+ @patient0 = ResourceGenerator.minimal_patient("#{Time.now.to_i}", 'Transaction', namespace: version_namespace)
patient0_id = SecureRandom.uuid
patient0_uri = "urn:uuid:#{patient0_id}"
# height
- @obs0a = ResourceGenerator.minimal_observation('http://loinc.org','8302-2',170,'cm',patient0_id)
+ @obs0a = ResourceGenerator.minimal_observation('http://loinc.org', '8302-2', 170, 'cm', patient0_id, namespace: version_namespace)
@obs0a.subject.reference = patient0_uri
# weight
- @obs0b = ResourceGenerator.minimal_observation('http://loinc.org','3141-9',200,'kg',patient0_id)
+ @obs0b = ResourceGenerator.minimal_observation('http://loinc.org', '3141-9', 200, 'kg', patient0_id, namespace: version_namespace)
@obs0b.subject.reference = patient0_uri
# obesity
- @condition0 = ResourceGenerator.minimal_condition('http://snomed.info/sct','414915002',patient0_id, version_namespace, patient0_uri)
+ @condition0 = ResourceGenerator.minimal_condition(
+ 'http://snomed.info/sct',
+ '414915002',
+ patient0_id,
+ namespace: version_namespace,
+ patient_ref: patient0_uri
+ )
@client.begin_transaction
@client.add_transaction_request('POST',nil,@patient0).fullUrl = patient0_uri
@@ -134,7 +141,7 @@ def teardown
assert @created_patient_record, 'Could not create patient in XFER0.'
# patient has gained weight
- @obs1 = ResourceGenerator.minimal_observation('http://loinc.org','3141-9',250,'kg',@patient0.id, version_namespace)
+ @obs1 = ResourceGenerator.minimal_observation('http://loinc.org', '3141-9', 250, 'kg', @patient0.id, namespace: version_namespace)
@client.begin_transaction
@client.add_transaction_request('POST',nil,@patient0,"identifier=#{@patient0.identifier.first.system}|#{@patient0.identifier.first.value}").fullUrl = "urn:uuid:#{SecureRandom.uuid}"
@@ -170,7 +177,7 @@ def teardown
assert @created_patient_record, 'Could not create patient in XFER0.'
# weight
- @obs2 = ResourceGenerator.minimal_observation('http://loinc.org','3141-9',100,'kg',@patient0.id, version_namespace)
+ @obs2 = ResourceGenerator.minimal_observation('http://loinc.org', '3141-9', 100, 'kg', @patient0.id, namespace: version_namespace)
# obesity has been refuted
if fhir_version == :dstu2
@condition0.patient.reference = "Patient/#{@patient0.id}"
@@ -214,7 +221,7 @@ def teardown
}
assert @created_patient_record, 'Could not create patient in XFER0.'
- @patient1 = ResourceGenerator.minimal_patient(@patient0.identifier.first.value,@patient0.name.first.given.first, version_namespace)
+ @patient1 = ResourceGenerator.minimal_patient(@patient0.identifier.first.value, @patient0.name.first.given.first, namespace: version_namespace)
reply = @client.create @patient1
assert_response_ok(reply)
@patient1.id = (reply.resource.try(:id) || reply.id)
@@ -253,9 +260,9 @@ def teardown
assert @created_patient_record, 'Could not create patient in XFER0.'
# height observation
- @obs3 = ResourceGenerator.minimal_observation('http://loinc.org','8302-2',177,'cm',@patient0.id, version_namespace)
+ @obs3 = ResourceGenerator.minimal_observation('http://loinc.org', '8302-2', 177, 'cm', @patient0.id, namespace: version_namespace)
# weight observation
- @obs4 = ResourceGenerator.minimal_observation('http://loinc.org','3141-9',105,'kg',@patient0.id, version_namespace)
+ @obs4 = ResourceGenerator.minimal_observation('http://loinc.org', '3141-9', 105, 'kg', @patient0.id, namespace: version_namespace)
# give this *weight* observation the ID of the *height* observation created in XFER1
@obs4.id = @obs0a.id
@@ -435,10 +442,10 @@ def teardown
skip 'TODO: https://github.com/FirelyTeam/spark/issues/306'
- @batch_patient = ResourceGenerator.minimal_patient("#{Time.now.to_i}",'Batch', version_namespace)
+ @batch_patient = ResourceGenerator.minimal_patient("#{Time.now.to_i}", 'Batch', namespace: version_namespace)
@batch_patient_id = "urn:uuid:#{SecureRandom.uuid}" # assign an id so related resources can reference the patient
# height
- @batch_obs = ResourceGenerator.minimal_observation('http://loinc.org','8302-2',900,'cm',@batch_patient_id, version_namespace)
+ @batch_obs = ResourceGenerator.minimal_observation('http://loinc.org', '8302-2', 900, 'cm', @batch_patient_id, namespace: version_namespace)
@batch_obs.subject.reference = @batch_patient_id
@client.begin_batch
@@ -478,15 +485,15 @@ def teardown
skip 'TODO: https://github.com/FirelyTeam/spark/issues/305'
- @batch_patient_2 = ResourceGenerator.minimal_patient("#{Time.now.to_i}",'Batch', version_namespace)
+ @batch_patient_2 = ResourceGenerator.minimal_patient("#{Time.now.to_i}", 'Batch', namespace: version_namespace)
reply = @client.create @batch_patient_2
assert_response_ok(reply)
@batch_patient_2.id = (reply.resource.try(:id) || reply.id)
# height
- @batch_obs_2 = ResourceGenerator.minimal_observation('http://loinc.org','8302-2',300,'cm',@batch_patient_2.id, version_namespace)
+ @batch_obs_2 = ResourceGenerator.minimal_observation('http://loinc.org', '8302-2', 300, 'cm', @batch_patient_2.id, namespace: version_namespace)
# weight
- @batch_obs_3 = ResourceGenerator.minimal_observation('http://loinc.org','3141-9',500,'kg',@batch_patient_2.id, version_namespace)
+ @batch_obs_3 = ResourceGenerator.minimal_observation('http://loinc.org', '3141-9', 500, 'kg', @batch_patient_2.id, namespace: version_namespace)
@client.begin_batch
@client.add_batch_request('POST',nil,@batch_obs_2).fullUrl = "urn:uuid:#{SecureRandom.uuid}"
@@ -553,7 +560,7 @@ def teardown
}
# Create a Patient and capture the ETag from the response
- @ifmatch_patient = ResourceGenerator.minimal_patient("#{Time.now.to_i}",'IfMatch', version_namespace)
+ @ifmatch_patient = ResourceGenerator.minimal_patient("#{Time.now.to_i}", 'IfMatch', namespace: version_namespace)
reply = @client.create @ifmatch_patient
assert_response_ok(reply)
@ifmatch_patient.id = (reply.resource.try(:id) || reply.id)
@@ -586,7 +593,7 @@ def teardown
}
# Create a Patient
- @ifmatch_patient_2 = ResourceGenerator.minimal_patient("#{Time.now.to_i}",'IfMatch2', version_namespace)
+ @ifmatch_patient_2 = ResourceGenerator.minimal_patient("#{Time.now.to_i}", 'IfMatch2', namespace: version_namespace)
reply = @client.create @ifmatch_patient_2
assert_response_ok(reply)
@ifmatch_patient_2.id = (reply.resource.try(:id) || reply.id)
diff --git a/lib/tests/testscripts/base_testscript.rb b/lib/tests/testscripts/base_testscript.rb
index 7ce9cf7a..88590b1b 100644
--- a/lib/tests/testscripts/base_testscript.rb
+++ b/lib/tests/testscripts/base_testscript.rb
@@ -78,6 +78,14 @@ def initialize(testscript, client, client2=nil)
load_fixtures
end
+ def version_namespace
+ if @client && @client.fhir_version != :stu3
+ raise ArgumentError, "FHIR TestScripts require STU3, got #{@client.fhir_version}"
+ end
+
+ FHIR::STU3
+ end
+
def author
@testscript.name
end
diff --git a/lib/tests/testscripts/testscript_engine.rb b/lib/tests/testscripts/testscript_engine.rb
index 85a11af1..4eaa6a9b 100644
--- a/lib/tests/testscripts/testscript_engine.rb
+++ b/lib/tests/testscripts/testscript_engine.rb
@@ -8,7 +8,7 @@ def initialize(client=nil, client2=nil)
@client = client
@client2 = client2
@scripts = []
- load_testscripts if client&.fhir_version != :dstu2 # Run tests scripts on STU3+ only.
+ load_testscripts if client.nil? || client.fhir_version == :stu3
end
def tests
diff --git a/lib/uscore_resource_generator.rb b/lib/uscore_resource_generator.rb
index 59bed11c..116400fb 100644
--- a/lib/uscore_resource_generator.rb
+++ b/lib/uscore_resource_generator.rb
@@ -6,7 +6,7 @@ class USCoreResourceGenerator < ResourceGenerator
# If we add another version, we may need to update these
def self.patient(identifier='0',name='Name')
- resource = minimal_patient(identifier,name)
+ resource = minimal_patient(identifier, name, namespace: FHIR)
# resource.identifier = [ minimal_identifier(identifier) ]
# resource.name = [ minimal_humanname(name) ]
resource.meta.profile = ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']
@@ -17,7 +17,7 @@ def self.patient(identifier='0',name='Name')
resource.birthDate = DateTime.now.strftime("%Y-%m-%d")
resource.deceasedBoolean = false
resource.address = [ address ]
- resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus','S')
+ resource.maritalStatus = minimal_codeableconcept('http://hl7.org/fhir/v3/MaritalStatus', 'S', namespace: FHIR)
resource.multipleBirthBoolean = false
resource.contact = [ patient_contact ]
resource.communication = [ patient_communication ]
@@ -26,8 +26,8 @@ def self.patient(identifier='0',name='Name')
resource.managingOrganization = FHIR::Reference.new # reference to US Core-Organization
resource.managingOrganization.display = 'US Core Organization'
resource.extension = []
- resource.extension << make_extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-race','CodeableConcept',minimal_codeableconcept('http://hl7.org/fhir/v3/Race','2106-3'))
- resource.extension << make_extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity','CodeableConcept',minimal_codeableconcept('http://hl7.org/fhir/v3/Ethnicity','2186-5'))
+ resource.extension << make_extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-race','CodeableConcept',minimal_codeableconcept('http://hl7.org/fhir/v3/Race', '2106-3', namespace: FHIR))
+ resource.extension << make_extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity','CodeableConcept',minimal_codeableconcept('http://hl7.org/fhir/v3/Ethnicity', '2186-5', namespace: FHIR))
resource.extension << make_extension('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName','String','Liberty')
resource.extension << make_extension('http://hl7.org/fhir/StructureDefinition/birthPlace','Address',address)
resource
@@ -70,8 +70,11 @@ def self.address
def self.patient_contact
resource = FHIR::Patient::Contact.new
- resource.relationship = [ minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','parent'), minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship','emergency') ]
- resource.name = minimal_humanname('Mom')
+ resource.relationship = [
+ minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship', 'parent', namespace: FHIR),
+ minimal_codeableconcept('http://hl7.org/fhir/patient-contact-relationship', 'emergency', namespace: FHIR)
+ ]
+ resource.name = minimal_humanname('Mom', namespace: FHIR)
resource.telecom = [ contact_point ]
resource.address = address
resource
@@ -79,7 +82,7 @@ def self.patient_contact
def self.patient_communication
resource = FHIR::Patient::Communication.new
- resource.language = minimal_codeableconcept('http://tools.ietf.org/html/bcp47','en-US')
+ resource.language = minimal_codeableconcept('http://tools.ietf.org/html/bcp47', 'en-US', namespace: FHIR)
resource
end
diff --git a/test/unit/fhir_structure_generator_test.rb b/test/unit/fhir_structure_generator_test.rb
new file mode 100644
index 00000000..06354b3c
--- /dev/null
+++ b/test/unit/fhir_structure_generator_test.rb
@@ -0,0 +1,89 @@
+require_relative '../test_helper'
+
+class FHIRStructureGeneratorTest < Test::Unit::TestCase
+ def test_generates_resource_categories_from_structure_definitions
+ structure = Crucible::FHIRStructureGenerator.generate(definitions, template)
+ resources = structure.fetch('children').find { |child| child['name'] == 'RESOURCES' }
+ evidence = resources.fetch('children').first.fetch('children').first
+
+ assert_equal 'Evidence-Based Medicine', evidence['name']
+ assert_equal ['citation', 'research definition'], evidence.fetch('children').map { |child| child['name'] }
+ end
+
+ def test_does_not_modify_the_template
+ original = JSON.generate(template)
+
+ Crucible::FHIRStructureGenerator.generate(definitions, template)
+
+ assert_equal original, JSON.generate(template)
+ end
+
+ def test_rejects_uncategorized_resources_without_an_explicit_override
+ uncategorized = definitions
+ uncategorized['entry'].first['resource']['extension'] = []
+
+ assert_raise(RuntimeError) do
+ Crucible::FHIRStructureGenerator.generate(uncategorized, template)
+ end
+ end
+
+ private
+
+ def template
+ {
+ 'name' => 'FHIR',
+ 'children' => [
+ {
+ 'name' => 'RESOURCES',
+ 'children' => [
+ {
+ 'name' => 'Specialized',
+ 'children' => [
+ { 'name' => 'Evidence-Based Medicine', 'children' => [{ 'name' => 'old resource' }] }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ def definitions
+ {
+ 'entry' => [
+ {
+ 'resource' => {
+ 'name' => 'Citation',
+ 'kind' => 'resource',
+ 'derivation' => 'specialization',
+ 'abstract' => false,
+ 'extension' => [
+ {
+ 'url' => Crucible::FHIRStructureGenerator::CATEGORY_URL,
+ 'valueString' => 'Specialized.Evidence-Based Medicine'
+ }
+ ]
+ }
+ },
+ {
+ 'resource' => {
+ 'name' => 'ResearchDefinition',
+ 'kind' => 'resource',
+ 'derivation' => 'specialization',
+ 'abstract' => false,
+ 'extension' => []
+ }
+ },
+ {
+ 'resource' => {
+ 'name' => 'Resource',
+ 'kind' => 'resource',
+ 'derivation' => 'specialization',
+ 'abstract' => true,
+ 'extension' => []
+ }
+ }
+ ]
+ }
+ end
+end
diff --git a/test/unit/fhir_structure_test.rb b/test/unit/fhir_structure_test.rb
index 65d453ab..4b0a86cc 100644
--- a/test/unit/fhir_structure_test.rb
+++ b/test/unit/fhir_structure_test.rb
@@ -4,21 +4,26 @@ class FHIRStructureTest < Test::Unit::TestCase
def test_fhir_starburst_root
structure = Crucible::FHIRStructure.get(:r4)
- structure['name'] == 'FHIR'
+ assert_equal 'FHIR', structure['name']
+ end
+
+ def test_fhir_starburst_root_r4b
+ structure = Crucible::FHIRStructure.get(:r4b)
+ assert_equal 'FHIR', structure['name']
end
def test_fhir_starburst_stu3
structure = Crucible::FHIRStructure.get(:stu3)
- structure['name'] == 'FHIR'
+ assert_equal 'FHIR', structure['name']
end
def test_fhir_starburst_root_dstu2
structure = Crucible::FHIRStructure.get(:dstu2)
- structure['name'] == 'FHIR'
+ assert_equal 'FHIR', structure['name']
end
def test_no_duplicate_names_in_starburst
- [:stu3, :dstu2].each do |version|
+ Crucible::FHIRVersion::KNOWN.each do |version|
structure = Crucible::FHIRStructure.get(version)
names = all_names(structure)
@@ -26,19 +31,13 @@ def test_no_duplicate_names_in_starburst
end
end
- def fhir_resources(fhir_version=nil)
-
- resources = FHIR::RESOURCES
- namespace = 'FHIR'
- if !fhir_version.nil? && FHIR.constants.include?(fhir_version.upcase)
- resources = FHIR.const_get(fhir_version.upcase)::RESOURCES
- end
- resources
+ def fhir_resources(fhir_version)
+ Crucible::FHIRVersion.namespace(fhir_version).const_get(:RESOURCES)
end
def test_no_missing_resources_in_starburst
- [:r4, :stu3, :dstu2].each do |version|
+ Crucible::FHIRVersion::KNOWN.each do |version|
structure = Crucible::FHIRStructure.get(version)
resource_subset = structure['children'].select{|c| c['name'] == 'RESOURCES'}.first
structure_resources = all_names(resource_subset, true).map{|e| e.downcase.delete(' ')}
@@ -61,7 +60,7 @@ def test_no_unknown_requires_in_tests
names = []
- [:dstu2, :stu3, :r4].each do |version|
+ Crucible::FHIRVersion::KNOWN.each do |version|
structure = Crucible::FHIRStructure.get(version)
names.concat(all_names(structure).map{|e| e.downcase.delete(' ')})
end
diff --git a/test/unit/fhir_version_test.rb b/test/unit/fhir_version_test.rb
new file mode 100644
index 00000000..661737be
--- /dev/null
+++ b/test/unit/fhir_version_test.rb
@@ -0,0 +1,58 @@
+require_relative '../test_helper'
+
+class FHIRVersionTest < Test::Unit::TestCase
+ def test_omitted_version_is_rejected
+ assert_raise(ArgumentError) do
+ Crucible::FHIRVersion.resolve
+ end
+
+ [nil, '', ' '].each do |version|
+ error = assert_raise(Crucible::FHIRVersion::UnsupportedVersionError) do
+ Crucible::FHIRVersion.resolve(version)
+ end
+
+ assert_match(/FHIR version is required/, error.message)
+ end
+ end
+
+ def test_known_versions_are_resolved_explicitly
+ assert_equal :dstu2, Crucible::FHIRVersion.resolve(:dstu2)
+ assert_equal :stu3, Crucible::FHIRVersion.resolve('STU3')
+ assert_equal :r4, Crucible::FHIRVersion.resolve(:r4)
+ assert_equal :r4b, Crucible::FHIRVersion.resolve('R4B')
+ end
+
+ def test_known_versions_are_listed_in_one_registry
+ assert_equal [:dstu2, :stu3, :r4, :r4b], Crucible::FHIRVersion::KNOWN
+ end
+
+ def test_known_versions_resolve_to_explicit_model_namespaces
+ assert_same FHIR::DSTU2, Crucible::FHIRVersion.namespace(:dstu2)
+ assert_same FHIR::STU3, Crucible::FHIRVersion.namespace(:stu3)
+ assert_same FHIR, Crucible::FHIRVersion.namespace(:r4)
+ assert_same FHIR::R4B, Crucible::FHIRVersion.namespace(:r4b)
+ end
+
+ def test_model_classes_resolve_to_their_owning_version
+ assert_equal :dstu2, Crucible::FHIRVersion.for_class(FHIR::DSTU2::Patient)
+ assert_equal :stu3, Crucible::FHIRVersion.for_class(FHIR::STU3::Patient)
+ assert_equal :r4, Crucible::FHIRVersion.for_class(FHIR::Patient)
+ assert_equal :r4b, Crucible::FHIRVersion.for_class(FHIR::R4B::Patient)
+ assert_equal :r4b, Crucible::FHIRVersion.for_class(FHIR::R4B::Patient.new)
+ end
+
+ def test_unknown_version_fails_instead_of_falling_back_to_r4
+ error = assert_raise(Crucible::FHIRVersion::UnsupportedVersionError) do
+ Crucible::FHIRVersion.resolve('r5')
+ end
+
+ assert_match(/Unsupported FHIR version 'r5'/, error.message)
+ assert_match(/dstu2, stu3, r4, r4b/, error.message)
+ end
+
+ def test_unknown_fhir_4_version_fails_instead_of_falling_back_to_r4
+ assert_raise(Crucible::FHIRVersion::UnsupportedVersionError) do
+ Crucible::FHIRVersion.resolve('4.1.0')
+ end
+ end
+end
diff --git a/test/unit/fixture_selection_test.rb b/test/unit/fixture_selection_test.rb
new file mode 100644
index 00000000..eefa2a90
--- /dev/null
+++ b/test/unit/fixture_selection_test.rb
@@ -0,0 +1,44 @@
+require_relative '../test_helper'
+require 'tmpdir'
+
+class FixtureSelectionTest < Test::Unit::TestCase
+ def test_selects_version_specific_fixture_from_the_fixture_root
+ with_fixture_helper(:r4b) do |resources, directory|
+ write_patient(directory, 'patient.json', 'base')
+ write_patient(directory, 'patient.r4b.json', 'r4b')
+
+ patient = resources.load_fixture('patient', :json)
+
+ assert_instance_of FHIR::R4B::Patient, patient
+ assert_equal 'r4b', patient.id
+ end
+ end
+
+ def test_falls_back_to_the_base_fixture
+ with_fixture_helper(:r4b) do |resources, directory|
+ write_patient(directory, 'patient.json', 'base')
+
+ patient = resources.load_fixture('patient', :json)
+
+ assert_instance_of FHIR::R4B::Patient, patient
+ assert_equal 'base', patient.id
+ end
+ end
+
+ private
+
+ def with_fixture_helper(version)
+ Dir.mktmpdir do |directory|
+ resources = Crucible::Generator::Resources.new(version)
+ resources.define_singleton_method(:fixture_path) { directory }
+ yield resources, directory
+ end
+ end
+
+ def write_patient(directory, filename, id)
+ File.write(
+ File.join(directory, filename),
+ JSON.generate(resourceType: 'Patient', id: id)
+ )
+ end
+end
diff --git a/test/unit/format_suite_test.rb b/test/unit/format_suite_test.rb
new file mode 100644
index 00000000..230e1e96
--- /dev/null
+++ b/test/unit/format_suite_test.rb
@@ -0,0 +1,128 @@
+require_relative '../test_helper'
+require 'webmock/test_unit'
+
+class FormatSuiteTest < Test::Unit::TestCase
+ BASE_URL = 'http://format-suite.test/fhir'.freeze
+ PATIENT_ID = 'format-suite-patient'.freeze
+ FHIR_VERSIONS = {
+ stu3: '3.0.2',
+ r4: '4.0.1',
+ r4b: '4.3.0'
+ }.freeze
+
+ FHIR_VERSIONS.each do |version, specification_version|
+ define_method("test_format_suite_executes_all_cases_with_#{version}_resources") do
+ execute_format_suite(version, specification_version)
+ end
+ end
+
+ private
+
+ def execute_format_suite(version, specification_version)
+ @namespace = Crucible::FHIRVersion.namespace(version)
+ @client = FHIR::Client.new(BASE_URL, fhir_version: version)
+ @created_patient = nil
+ stub_capability_statement(specification_version)
+ stub_create
+ stub_reads
+ stub_delete
+
+ suite = Crucible::Tests::FormatTest.new(@client)
+ tests = suite.execute.fetch('Format001')
+
+ assert_equal 22, tests.length
+ assert_true tests.all? { |test| test['status'] == 'pass' }, failure_summary(tests)
+ assert_instance_of @namespace.const_get(:Patient), @created_patient
+ assert_include suite.supported_versions, version
+ end
+
+ def stub_capability_statement(specification_version)
+ capability_statement = @namespace.const_get(:CapabilityStatement).new(
+ status: 'active',
+ date: '2026-07-17',
+ kind: 'instance',
+ fhirVersion: specification_version,
+ format: ['xml', 'json']
+ )
+
+ stub_request(:get, "#{BASE_URL}/metadata").to_return(
+ status: 200,
+ body: capability_statement.to_json,
+ headers: { 'Content-Type' => FHIR::Formats::ResourceFormat::RESOURCE_JSON }
+ )
+ end
+
+ def stub_create
+ stub_request(:post, "#{BASE_URL}/Patient").to_return do |request|
+ @created_patient = @namespace.from_contents(request.body)
+ @created_patient.id = PATIENT_ID
+ @created_patient.meta ||= @namespace.const_get(:Meta).new
+ @created_patient.meta.versionId = '1'
+ @created_patient.meta.lastUpdated = '2026-07-17T12:00:00Z'
+
+ {
+ status: 201,
+ body: @created_patient.to_json,
+ headers: {
+ 'Content-Type' => FHIR::Formats::ResourceFormat::RESOURCE_JSON,
+ 'Location' => "#{BASE_URL}/Patient/#{PATIENT_ID}/_history/1"
+ }
+ }
+ end
+ end
+
+ def stub_reads
+ stub_request(:get, %r{\A#{Regexp.escape(BASE_URL)}/Patient(?:/#{PATIENT_ID})?(?:\?.*)?\z}).to_return do |request|
+ requested_format = request_format(request)
+ next { status: 406 } if requested_format.include?('application/foobar')
+
+ resource = if request.uri.path.end_with?("/Patient/#{PATIENT_ID}")
+ @created_patient
+ else
+ search_bundle
+ end
+ xml = requested_format.downcase.include?('xml')
+
+ {
+ status: 200,
+ body: xml ? resource.to_xml : resource.to_json,
+ headers: {
+ 'Content-Type' => xml ? FHIR::Formats::ResourceFormat::RESOURCE_XML :
+ FHIR::Formats::ResourceFormat::RESOURCE_JSON
+ }
+ }
+ end
+ end
+
+ def stub_delete
+ stub_request(
+ :delete,
+ %r{\A#{Regexp.escape(BASE_URL)}/Patient/#{PATIENT_ID}(?:\?.*)?\z}
+ ).to_return(status: 204)
+ end
+
+ def request_format(request)
+ query_format = request.uri.query_values&.fetch('_format', nil)
+ (query_format || request.headers['Accept'] || '').to_s
+ end
+
+ def search_bundle
+ bundle_class = @namespace.const_get(:Bundle)
+ bundle_class.new(
+ type: 'searchset',
+ total: 1,
+ entry: [
+ {
+ 'fullUrl' => "#{BASE_URL}/Patient/#{PATIENT_ID}",
+ 'resource' => @created_patient.to_hash
+ }
+ ]
+ )
+ end
+
+ def failure_summary(tests)
+ tests.reject { |test| test['status'] == 'pass' }.map do |test|
+ "#{test[:test_method]}: #{test['status']} #{test['message']}"
+ end.join("\n")
+ end
+end
diff --git a/test/unit/metadata_test.rb b/test/unit/metadata_test.rb
index c58db8d1..15b3c2d1 100644
--- a/test/unit/metadata_test.rb
+++ b/test/unit/metadata_test.rb
@@ -40,4 +40,10 @@ def test_testscript_find
assert !keyed_test.nil?, "Failed to find testscript by key"
end
+ def test_testscripts_are_not_loaded_for_non_stu3_clients
+ client = FHIR::Client.new('http://r4b', fhir_version: :r4b)
+
+ assert_empty Crucible::Tests::TestScriptEngine.new(client).tests
+ end
+
end
diff --git a/test/unit/r4b_routing_test.rb b/test/unit/r4b_routing_test.rb
new file mode 100644
index 00000000..c288b409
--- /dev/null
+++ b/test/unit/r4b_routing_test.rb
@@ -0,0 +1,113 @@
+require_relative '../test_helper'
+
+class R4BRoutingTest < Test::Unit::TestCase
+ def setup
+ @client = FHIR::Client.new('http://r4b', fhir_version: :r4b)
+ @suite = Crucible::Tests::BaseSuite.new(@client)
+ end
+
+ def test_client_extension_does_not_replace_r4b_selection
+ assert_equal :r4b, @client.fhir_version
+ end
+
+ def test_base_test_uses_r4b_namespace
+ assert_same FHIR::R4B, @suite.version_namespace
+ end
+
+ def test_base_suite_resolves_r4b_resources
+ assert_same FHIR::R4B::Citation, Crucible::Tests::BaseSuite.get_resource(:r4b, :Citation)
+ assert_true Crucible::Tests::BaseSuite.valid_resource?(:r4b, 'Citation')
+ assert_false Crucible::Tests::BaseSuite.valid_resource?(:r4, 'Citation')
+ end
+
+ def test_base_suite_parses_r4b_resources
+ patient = @suite.resource_from_contents(FHIR::R4B::Patient.new(id: 'r4b').to_json)
+ outcome = @suite.parse_operation_outcome(
+ FHIR::R4B::OperationOutcome.new(issue: [{ severity: 'error', code: 'invalid' }]).to_json
+ )
+
+ assert_instance_of FHIR::R4B::Patient, patient
+ assert_instance_of FHIR::R4B::OperationOutcome, outcome
+ end
+
+ def test_r4b_resource_enumeration_stays_in_r4b_namespace
+ resources = Crucible::Tests::BaseSuite.fhir_resources(:r4b)
+
+ assert_not_empty resources
+ assert_true resources.all? { |resource| resource.name.start_with?('FHIR::R4B::') }
+ end
+
+ def test_resource_generator_uses_r4b_types
+ patient = Crucible::Tests::ResourceGenerator.generate(FHIR::R4B::Patient)
+
+ assert_instance_of FHIR::R4B::Patient, patient
+ assert_instance_of FHIR::R4B::Meta, patient.meta
+ end
+
+ def test_minimal_resource_helpers_require_an_explicit_namespace
+ assert_raise(ArgumentError) do
+ Crucible::Tests::ResourceGenerator.minimal_patient
+ end
+ end
+
+ def test_minimal_patient_keeps_all_children_in_r4b_namespace
+ patient = Crucible::Tests::ResourceGenerator.minimal_patient(namespace: FHIR::R4B)
+
+ assert_instance_of FHIR::R4B::Patient, patient
+ assert_instance_of FHIR::R4B::Identifier, patient.identifier.first
+ assert_instance_of FHIR::R4B::HumanName, patient.name.first
+ assert_instance_of FHIR::R4B::Meta, patient.meta
+ assert_instance_of FHIR::R4B::Coding, patient.meta.tag.first
+ end
+
+ def test_minimal_condition_uses_r4b_status_datatype
+ condition = Crucible::Tests::ResourceGenerator.minimal_condition(namespace: FHIR::R4B)
+
+ assert_instance_of FHIR::R4B::Condition, condition
+ assert_instance_of FHIR::R4B::CodeableConcept, condition.verificationStatus
+ assert_instance_of FHIR::R4B::Coding, condition.verificationStatus.coding.first
+ assert_equal 'confirmed', condition.verificationStatus.coding.first.code
+ end
+
+ def test_condition_status_conversion_preserves_each_version_namespace
+ r4_condition = FHIR::Condition.new
+ r4_condition.clinicalStatus = 'active'
+ r4_condition.verificationStatus = 'confirmed'
+ r4b_condition = FHIR::R4B::Condition.new
+ r4b_condition.clinicalStatus = 'active'
+ r4b_condition.verificationStatus = 'confirmed'
+ stu3_condition = FHIR::STU3::Condition.new
+ stu3_condition.clinicalStatus = 'active'
+ stu3_condition.verificationStatus = 'confirmed'
+
+ Crucible::Tests::ResourceGenerator.fix_condition(r4_condition)
+ Crucible::Tests::ResourceGenerator.fix_condition(r4b_condition)
+ Crucible::Tests::ResourceGenerator.fix_condition(stu3_condition)
+
+ assert_instance_of FHIR::CodeableConcept, r4_condition.clinicalStatus
+ assert_instance_of FHIR::CodeableConcept, r4_condition.verificationStatus
+ assert_instance_of FHIR::R4B::CodeableConcept, r4b_condition.clinicalStatus
+ assert_instance_of FHIR::R4B::CodeableConcept, r4b_condition.verificationStatus
+ assert_equal 'active', stu3_condition.clinicalStatus
+ assert_equal 'confirmed', stu3_condition.verificationStatus
+ end
+
+ def test_resource_fixture_helper_uses_r4b_namespace
+ resources = Crucible::Generator::Resources.new(:r4b)
+
+ assert_same FHIR::R4B, resources.instance_variable_get(:@namespace)
+ end
+
+ def test_resource_suite_metadata_advertises_r4b_support
+ resource_test = Crucible::Tests::ResourceTest.new(nil)
+ resource_suite_metadata = Crucible::Tests::SuiteEngine.list_all.values.select do |metadata|
+ metadata.key?('resource_class')
+ end
+ advertises_r4b = resource_suite_metadata.any? do |metadata|
+ metadata['supported_versions'].include?(:r4b)
+ end
+
+ assert_include resource_test.supported_versions, :r4b
+ assert_true advertises_r4b
+ end
+end
diff --git a/test/unit/resource_generator_test.rb b/test/unit/resource_generator_test.rb
index b86b4231..dbd68f8e 100644
--- a/test/unit/resource_generator_test.rb
+++ b/test/unit/resource_generator_test.rb
@@ -39,6 +39,76 @@ class ResourceGeneratorTest < Test::Unit::TestCase
end
end
+ def test_empty_r4b_codeable_reference_gets_a_concept
+ reference = Crucible::Tests::ResourceGenerator.generate(FHIR::R4B::CodeableReference)
+
+ assert_instance_of FHIR::R4B::CodeableConcept, reference.concept
+ assert_not_empty reference.concept.text
+ assert_nil reference.reference
+ end
+
+ def test_populated_r4b_codeable_reference_is_preserved
+ reference = FHIR::R4B::CodeableReference.new
+ reference.reference = FHIR::R4B::Reference.new(display: 'Existing reference')
+
+ Crucible::Tests::ResourceGenerator.apply_invariants!(reference)
+
+ assert_nil reference.concept
+ assert_equal 'Existing reference', reference.reference.display
+ end
+
+ def test_r4b_packaged_product_definition_has_valid_contained_items
+ resource = Crucible::Tests::ResourceGenerator.generate(FHIR::R4B::PackagedProductDefinition, 5)
+ packages = [resource.package].compact
+ contained_items = []
+
+ until packages.empty?
+ package = packages.shift
+ contained_items.concat(package.containedItem || [])
+ packages.concat(package.package || [])
+ end
+
+ assert_not_empty contained_items
+ assert_true contained_items.all? do |contained_item|
+ contained_item.item && (contained_item.item.concept || contained_item.item.reference)
+ end
+ assert_empty FHIR::R4B::Xml.validate(resource.to_xml).map(&:message)
+ end
+
+ def test_r4b_questionnaire_selectable_codes_exclude_abstract_question
+ metadata = FHIR::R4B::Questionnaire::Item::METADATA['type']
+ generated_codes = Crucible::Tests::ResourceGenerator.selectable_valid_codes(metadata, 'FHIR::R4B')
+
+ assert_include metadata['valid_codes']['http://hl7.org/fhir/item-type'], 'question'
+ assert_not_include generated_codes['http://hl7.org/fhir/item-type'], 'question'
+ assert_include generated_codes['http://hl7.org/fhir/item-type'], 'string'
+ end
+
+ def test_selectable_code_cache_preserves_each_fields_metadata_subset
+ generator = Crucible::Tests::ResourceGenerator
+ generator.remove_instance_variable(:@selectable_expansion_codes_cache) if generator.instance_variable_defined?(:@selectable_expansion_codes_cache)
+ metadata = FHIR::R4B::Questionnaire::Item::METADATA['type']
+ generator.selectable_valid_codes(metadata, 'FHIR::R4B')
+ subset_metadata = metadata.deep_dup
+ subset_metadata['valid_codes'] = { 'http://hl7.org/fhir/item-type' => ['string'] }
+
+ generated_codes = generator.selectable_valid_codes(subset_metadata, 'FHIR::R4B')
+
+ assert_equal({ 'http://hl7.org/fhir/item-type' => ['string'] }, generated_codes)
+ end
+
+ def test_generated_r4b_questionnaire_items_use_selectable_types
+ resource = Crucible::Tests::ResourceGenerator.generate(FHIR::R4B::Questionnaire, 5)
+ items = questionnaire_items(resource.item)
+ selectable_types = Crucible::Tests::ResourceGenerator.selectable_valid_codes(
+ FHIR::R4B::Questionnaire::Item::METADATA['type'],
+ 'FHIR::R4B'
+ ).values.flatten
+
+ assert_not_empty items
+ assert_true items.all? { |item| selectable_types.include?(item.type) }
+ end
+
def run_generator(resource_type, version, max_depth)
klass_namespace = "FHIR"
@@ -70,6 +140,10 @@ def check_valid_namespaces(resource, namespace)
return resource.instance_values.values.all? { |v| check_valid_namespaces(v, namespace) }
end
+ def questionnaire_items(items)
+ items.to_a.flat_map { |item| [item] + questionnaire_items(item.item) }
+ end
+
def test_valid_oid_generator
500.times do
random_oid = Crucible::Tests::ResourceGenerator.random_oid
diff --git a/test/unit/supported_versions_test.rb b/test/unit/supported_versions_test.rb
new file mode 100644
index 00000000..7288def1
--- /dev/null
+++ b/test/unit/supported_versions_test.rb
@@ -0,0 +1,28 @@
+require_relative '../test_helper'
+
+class SupportedVersionsTest < Test::Unit::TestCase
+ def test_base_suite_does_not_grant_implicit_version_support
+ assert_empty Crucible::Tests::BaseSuite.new(nil).supported_versions
+ end
+
+ def test_every_executable_suite_declares_supported_versions
+ suites = Crucible::Tests::SuiteEngine.new.tests
+
+ assert_true suites.all? { |suite| suite.supported_versions.any? }
+ end
+
+ def test_resource_suites_preserve_their_existing_version_support
+ expected = [:dstu2, :stu3, :r4, :r4b]
+
+ assert_equal expected, Crucible::Tests::ResourceTest.new(nil).supported_versions
+ assert_equal expected, Crucible::Tests::SearchTest.new(nil).supported_versions
+ end
+
+ def test_every_r4_suite_advertises_r4b
+ suites = Crucible::Tests::SuiteEngine.new.tests
+ r4_suites = suites.select { |suite| suite.supported_versions.include?(:r4) }
+ r4b_suites = suites.select { |suite| suite.supported_versions.include?(:r4b) }
+
+ assert_equal r4_suites.map(&:class).sort_by(&:name), r4b_suites.map(&:class).sort_by(&:name)
+ end
+end