From ecf8bda46f783d4227d123e95f9900df44b66084 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Mon, 18 Nov 2024 19:53:58 +0100 Subject: [PATCH] [WIP] Implement ValueSet References Closes: #110 --- .clj-kondo/root/config.edn | 1 + .github/scripts/value-set-expand/expand.sh | 21 + .github/scripts/value-set-expand/upload.sh | 17 + .github/workflows/build.yml | 35 ++ deps.edn | 10 +- docs/api.md | 1 + docs/api/operation-value-set-expand.md | 15 + docs/conformance/cql.md | 6 +- modules/cql/deps.edn | 3 + .../blaze/elm/compiler/clinical_operators.clj | 32 +- .../elm/compiler/clinical_operators/impl.clj | 9 + .../blaze/elm/compiler/clinical_values.clj | 36 +- modules/cql/src/blaze/elm/compiler/core.clj | 22 +- .../src/blaze/elm/compiler/external_data.clj | 2 + .../cql/src/blaze/elm/compiler/library.clj | 4 +- .../src/blaze/elm/compiler/library/spec.clj | 4 + .../src/blaze/elm/compiler/library_spec.clj | 2 +- modules/cql/src/blaze/elm/concept.clj | 3 + modules/cql/src/blaze/elm/spec.clj | 45 +- modules/cql/src/blaze/elm/value_set.clj | 27 ++ .../blaze/elm/compiler/library_test_perf.clj | 9 +- .../cql/test/blaze/cql/translator_test.clj | 24 ++ .../compiler/clinical_operators/impl_test.clj | 30 ++ .../elm/compiler/clinical_operators_test.clj | 157 ++++++- .../elm/compiler/clinical_values_test.clj | 85 +++- .../blaze/elm/compiler/external_data_test.clj | 19 +- .../test/blaze/elm/compiler/library_test.clj | 151 ++++--- modules/cql/test/blaze/elm/literal.clj | 10 + modules/cql/test/data_readers.clj | 2 + .../src/blaze/terminology_service/extern.clj | 38 +- .../blaze/terminology_service/extern_test.clj | 1 + .../src/blaze/fhir/spec/spec.clj | 3 + .../blaze/fhir/operation/evaluate_measure.clj | 4 +- .../operation/evaluate_measure/measure.clj | 19 +- .../operation/evaluate_measure/cql_test.clj | 13 +- .../measure/stratifier_test.clj | 13 +- .../evaluate_measure/measure_spec.clj | 3 +- .../evaluate_measure/measure_test.clj | 90 ++-- .../q61-in-value-set-gender.cql | 10 + .../q61-in-value-set-gender.json | 133 ++++++ .../fhir/operation/evaluate_measure_test.clj | 46 +- .../.clj-kondo/config.edn | 9 + modules/operation-value-set-expand/Makefile | 25 ++ modules/operation-value-set-expand/README.md | 15 + modules/operation-value-set-expand/deps.edn | 31 ++ .../blaze/fhir/operation/value_set/expand.clj | 32 ++ .../fhir/operation/value_set/expand_test.clj | 153 +++++++ modules/operation-value-set-expand/tests.edn | 10 + .../test/blaze/db/kv/rocksdb/impl_test.clj | 8 +- .../terminology-service/.clj-kondo/config.edn | 5 +- modules/terminology-service/Makefile | 18 +- modules/terminology-service/deps.edn | 26 +- .../src/blaze/terminology_service.clj | 23 +- .../src/blaze/terminology_service/local.clj | 122 ++++++ .../blaze/terminology_service/protocols.clj | 4 + .../src/blaze/terminology_service/spec.clj | 19 + .../src/blaze/terminology_service_spec.clj | 21 +- .../blaze/terminology_service/local_test.clj | 408 ++++++++++++++++++ modules/terminology-service/tests.edn | 11 + resources/blaze.edn | 20 +- 60 files changed, 1894 insertions(+), 221 deletions(-) create mode 100755 .github/scripts/value-set-expand/expand.sh create mode 100755 .github/scripts/value-set-expand/upload.sh create mode 100644 docs/api/operation-value-set-expand.md create mode 100644 modules/cql/src/blaze/elm/compiler/clinical_operators/impl.clj create mode 100644 modules/cql/src/blaze/elm/value_set.clj create mode 100644 modules/cql/test/blaze/elm/compiler/clinical_operators/impl_test.clj create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.cql create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.json create mode 100644 modules/operation-value-set-expand/.clj-kondo/config.edn create mode 100644 modules/operation-value-set-expand/Makefile create mode 100644 modules/operation-value-set-expand/README.md create mode 100644 modules/operation-value-set-expand/deps.edn create mode 100644 modules/operation-value-set-expand/src/blaze/fhir/operation/value_set/expand.clj create mode 100644 modules/operation-value-set-expand/test/blaze/fhir/operation/value_set/expand_test.clj create mode 100644 modules/operation-value-set-expand/tests.edn create mode 100644 modules/terminology-service/src/blaze/terminology_service/local.clj create mode 100644 modules/terminology-service/src/blaze/terminology_service/protocols.clj create mode 100644 modules/terminology-service/src/blaze/terminology_service/spec.clj create mode 100644 modules/terminology-service/test/blaze/terminology_service/local_test.clj create mode 100644 modules/terminology-service/tests.edn diff --git a/.clj-kondo/root/config.edn b/.clj-kondo/root/config.edn index 658e09d2f..1573faa4e 100644 --- a/.clj-kondo/root/config.edn +++ b/.clj-kondo/root/config.edn @@ -80,6 +80,7 @@ blaze.middleware.fhir.db db blaze.rest-api.header header blaze.scheduler sched + blaze.terminology-service ts blaze.test-util tu blaze.util u buddy.auth auth diff --git a/.github/scripts/value-set-expand/expand.sh b/.github/scripts/value-set-expand/expand.sh new file mode 100755 index 000000000..a93ea7510 --- /dev/null +++ b/.github/scripts/value-set-expand/expand.sh @@ -0,0 +1,21 @@ +#!/bin/bash -e + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +. "$SCRIPT_DIR/../util.sh" + +BASE="http://localhost:8080/fhir" + +expand() { + curl -sH "Accept: application/fhir+json" "$BASE/ValueSet/\$expand?url=$1" | jq -r '[.expansion.contains[].code] | join(",")' +} + +test "Abrechnungsart" "$(expand "http://fhir.de/ValueSet/dkgev/Abrechnungsart")" "AOP,HSA,PIA,SPZ,ASV,KIA,DRG,PEPP,VNSB,AP,SPB,WLU,WLA,PS,SZ,KV,BG,SL,KEK,IA,MVZ,IV,DMP,REHA,PSY" +test "AbrechnungsDiagnoseProzedur" "$(expand "http://fhir.de/ValueSet/AbrechnungsDiagnoseProzedur")" "hospital-main-diagnosis,principle-DRG,secondary-DRG" +test "Diagnosesubtyp" "$(expand "http://fhir.de/ValueSet/Diagnosesubtyp")" "surgery-diagnosis,department-main-diagnosis,infection-control-diagnosis,cause-of-death,AD,DD" + +test "mii-vs-fall-diagnosis-use" "$(expand "https://www.medizininformatik-initiative.de/fhir/core/modul-fall/ValueSet/mii-vs-fall-diagnosis-use")" "referral-diagnosis,treatment-diagnosis,surgery-diagnosis,department-main-diagnosis,infection-control-diagnosis,cause-of-death,AD,DD,CC,CM,pre-op,post-op,billing" +test "identifier-type-codes" "$(expand "https://www.medizininformatik-initiative.de/fhir/core/modul-fall/ValueSet/identifier-type-codes")" "ACSN,BRN,DL,DR,EN,FILL,JHN,MCN,MD,MR,NIIP,PLAC,PPN,PRN,SB,SNO,TAX,UDI,VN" +test "location-physical-type" "$(expand "https://www.medizininformatik-initiative.de/fhir/core/modul-fall/ValueSet/location-physical-type")" "wa,ro,bd" +test "mii-vs-consent-answer" "$(expand "https://www.medizininformatik-initiative.de/fhir/modul-consent/ValueSet/mii-vs-consent-answer")" "2.16.840.1.113883.3.1937.777.24.5.2.3,2.16.840.1.113883.3.1937.777.24.5.2.2,2.16.840.1.113883.3.1937.777.24.5.2.1" +test "mii-vs-consent-policy" "$(expand "https://www.medizininformatik-initiative.de/fhir/modul-consent/ValueSet/mii-vs-consent-policy")" "2.16.840.1.113883.3.1937.777.24.5.3.1,2.16.840.1.113883.3.1937.777.24.5.3.44,2.16.840.1.113883.3.1937.777.24.5.3.48,2.16.840.1.113883.3.1937.777.24.5.3.10,2.16.840.1.113883.3.1937.777.24.5.3.14,2.16.840.1.113883.3.1937.777.24.5.3.18,2.16.840.1.113883.3.1937.777.24.5.3.24,2.16.840.1.113883.3.1937.777.24.5.3.50,2.16.840.1.113883.3.1937.777.24.5.3.54,2.16.840.1.113883.3.1937.777.24.5.3.26,2.16.840.1.113883.3.1937.777.24.5.3.30,2.16.840.1.113883.3.1937.777.24.5.3.32,2.16.840.1.113883.3.1937.777.24.5.3.35" +test "mii-vs-consent-signaturetypes" "$(expand "https://www.medizininformatik-initiative.de/fhir/modul-consent/ValueSet/mii-vs-consent-signaturetypes")" "1.2.840.10065.1.12.1.7,1.2.840.10065.1.12.1.11" diff --git a/.github/scripts/value-set-expand/upload.sh b/.github/scripts/value-set-expand/upload.sh new file mode 100755 index 000000000..55d71073a --- /dev/null +++ b/.github/scripts/value-set-expand/upload.sh @@ -0,0 +1,17 @@ +#!/bin/bash -e + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + +BASE="http://localhost:8080/fhir" + +# Upload CodeSystem resources +for file in "$SCRIPT_DIR"/CodeSystem-*.json; do + echo "Uploading $file..." + curl -H "Content-Type: application/fhir+json" -H "Prefer: return=minimal" -d @"$file" "$BASE/CodeSystem" +done + +# Upload ValueSet resources +for file in "$SCRIPT_DIR"/ValueSet-*.json; do + echo "Uploading $file..." + curl -H "Content-Type: application/fhir+json" -H "Prefer: return=minimal" -d @"$file" "$BASE/ValueSet" +done diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df50b5f52..b0ea04f98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1222,6 +1222,40 @@ jobs: - name: Docker Stats run: docker stats --no-stream + integration-test-value-set-expand: + needs: build + runs-on: ubuntu-24.04 + + steps: + - name: Check out Git repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Firely Terminal + run: dotnet tool install -g firely.terminal + + - name: Install Consent + run: fhir install de.medizininformatikinitiative.kerndatensatz.consent 1.0.7 + + - name: Download Blaze Image + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + name: blaze-image + path: /tmp + + - name: Load Blaze Image + run: docker load --input /tmp/blaze.tar + + - name: Run Blaze + run: docker run --name blaze -d -e JAVA_TOOL_OPTIONS=-Xmx2g -p 8080:8080 --read-only --tmpfs /tmp:exec -v blaze-data:/app/data blaze:latest + + - name: Wait for Blaze + run: .github/scripts/wait-for-url.sh http://localhost:8080/health + not-enforcing-referential-integrity-test: needs: build runs-on: ubuntu-24.04 @@ -2001,6 +2035,7 @@ jobs: - integration-test - integration-test-synthea-1000 - integration-test-patient-purge + - integration-test-value-set-expand - not-enforcing-referential-integrity-test - admin-api-test - small-transactions-test diff --git a/deps.edn b/deps.edn index 0436d3fcd..332576dbc 100644 --- a/deps.edn +++ b/deps.edn @@ -13,6 +13,9 @@ blaze/interaction {:local/root "modules/interaction"} + blaze/openid-auth + {:local/root "modules/openid-auth"} + blaze.operation/compact {:local/root "modules/operation-compact"} @@ -31,8 +34,8 @@ blaze.operation/totals {:local/root "modules/operation-totals"} - blaze/openid-auth - {:local/root "modules/openid-auth"} + blaze.operation/value-set-expand + {:local/root "modules/operation-value-set-expand"} blaze/page-store-cassandra {:local/root "modules/page-store-cassandra"} @@ -46,6 +49,9 @@ blaze/server {:local/root "modules/server"} + blaze/terminology-service + {:local/root "modules/terminology-service"} + blaze/thread-pool-executor-collector {:local/root "modules/thread-pool-executor-collector"} diff --git a/docs/api.md b/docs/api.md index bc74da1ff..392aca537 100644 --- a/docs/api.md +++ b/docs/api.md @@ -193,6 +193,7 @@ The following Operations are implemented: * [Measure $evaluate-measure](api/operation-measure-evaluate-measure.md) * [Patient $everything](api/operation-patient-everything.md) * [Patient $purge](api/operation-patient-purge.md) +* [ValueSet $expand](api/operation-value-set-expand.md) ## Asynchronous Requests diff --git a/docs/api/operation-value-set-expand.md b/docs/api/operation-value-set-expand.md new file mode 100644 index 000000000..9c9e36a35 --- /dev/null +++ b/docs/api/operation-value-set-expand.md @@ -0,0 +1,15 @@ +# Operation \$expand on ValueSet + +> [!CAUTION] +> The operation \$expand on ValueSet is currently **beta**. Only a basic functionality is implemented. + +The \$expand operation can be used to expand all codes of a ValueSet. + +``` +GET [base]/ValueSet/$expand +GET [base]/ValueSet/[id]/$expand +``` + +The official documentation can be found [here][1]. + +[1]: diff --git a/docs/conformance/cql.md b/docs/conformance/cql.md index 83ec5502c..0e0cab5e5 100644 --- a/docs/conformance/cql.md +++ b/docs/conformance/cql.md @@ -32,8 +32,8 @@ The section numbers refer to the documentation of the [ELM Specification](https: | 3.8. | ConceptRef | ✓ | | | | 3.9. | Quantity | ✓ | | | | 3.10. | Ratio | ✓ | | | -| 3.11. | ValueSetDef | ✗ | | | -| 3.12. | ValueSetRef | ✗ | | | +| 3.11. | ValueSetDef | ✓ | | | +| 3.12. | ValueSetRef | ✓ | | | ### 4. Type Specifiers @@ -373,7 +373,7 @@ The section numbers refer to the documentation of the [ELM Specification](https: | 23.5. | Equal | ✓ | | 23.6. | Equivalent | ✓ | | 23.7. | InCodeSystem | ✗ | -| 23.8. | InValueSet | ✗ | +| 23.8. | InValueSet | ✓ | | 23.9. | ExpandValueSet | ✗ | | 23.10. | Not Equal | ✓ | | 23.11. | SubsumedBy | ✗ | diff --git a/modules/cql/deps.edn b/modules/cql/deps.edn index 59350f717..8b7922085 100644 --- a/modules/cql/deps.edn +++ b/modules/cql/deps.edn @@ -4,6 +4,9 @@ {blaze/db {:local/root "../db"} + blaze/terminology-service + {:local/root "../terminology-service"} + com.fasterxml.jackson.module/jackson-module-jaxb-annotations {:mvn/version "2.18.1" :exclusions [javax.xml.bind/jaxb-api]} diff --git a/modules/cql/src/blaze/elm/compiler/clinical_operators.clj b/modules/cql/src/blaze/elm/compiler/clinical_operators.clj index 72799ac70..1e8ae49f5 100644 --- a/modules/cql/src/blaze/elm/compiler/clinical_operators.clj +++ b/modules/cql/src/blaze/elm/compiler/clinical_operators.clj @@ -4,9 +4,12 @@ Section numbers are according to https://cql.hl7.org/04-logicalspecification.html." (:require + [blaze.elm.compiler.clinical-operators.impl :as impl] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.macros :refer [reify-expr]] - [blaze.elm.protocols :as p])) + [blaze.elm.concept :as concept] + [blaze.elm.protocols :as p] + [blaze.elm.value-set :as value-set])) ;; 23.3. CalculateAge ;; @@ -42,3 +45,30 @@ (when-let [date (core/compile* context date)] (let [chrono-precision (some-> precision core/to-chrono-unit)] (calculate-age-at-op birth-date date chrono-precision precision))))) + +;; 23.8. InValueSet +(defn- in-value-set [code value-set] + (reify-expr core/Expression + (-attach-cache [_ cache] + (core/attach-cache-helper in-value-set cache code value-set)) + (-resolve-refs [_ expression-defs] + (core/resolve-refs-helper in-value-set expression-defs code value-set)) + (-resolve-params [_ parameters] + (core/resolve-params-helper in-value-set parameters code value-set)) + (-optimize [_ db] + (core/optimize-helper in-value-set db code value-set)) + (-eval [_ context resource scope] + (let [value-set (core/-eval value-set context resource scope) + code (core/-eval code context resource scope)] + (cond + (string? code) (contains? (into #{} (map :code) value-set) code) + (concept/concept? code) (impl/contains-any? value-set (:codes code)) + :else (contains? value-set (value-set/from-code code))))) + (-form [_] + (list 'in-value-set (core/-form code) (core/-form value-set))))) + +(defmethod core/compile* :elm.compiler.type/in-value-set + [context {:keys [code] value-set :valueset}] + (let [code (core/compile* context code)] + (when-let [value-set (core/compile* context value-set)] + (in-value-set code value-set)))) diff --git a/modules/cql/src/blaze/elm/compiler/clinical_operators/impl.clj b/modules/cql/src/blaze/elm/compiler/clinical_operators/impl.clj new file mode 100644 index 000000000..fb0594475 --- /dev/null +++ b/modules/cql/src/blaze/elm/compiler/clinical_operators/impl.clj @@ -0,0 +1,9 @@ +(ns blaze.elm.compiler.clinical-operators.impl + (:require + [blaze.elm.value-set :as value-set])) + +(defn contains-any? [value-set [code & codes]] + (cond + (nil? code) false + (contains? value-set (value-set/from-code code)) true + :else (recur value-set codes))) diff --git a/modules/cql/src/blaze/elm/compiler/clinical_values.clj b/modules/cql/src/blaze/elm/compiler/clinical_values.clj index 0eb477288..51380a14b 100644 --- a/modules/cql/src/blaze/elm/compiler/clinical_values.clj +++ b/modules/cql/src/blaze/elm/compiler/clinical_values.clj @@ -7,10 +7,14 @@ [blaze.anomaly :as ba :refer [throw-anom]] [blaze.elm.code :as code] [blaze.elm.compiler.core :as core] + [blaze.elm.compiler.macros :refer [reify-expr]] [blaze.elm.concept :refer [concept]] [blaze.elm.date-time :as date-time] [blaze.elm.quantity :refer [quantity]] - [blaze.elm.ratio :refer [ratio]])) + [blaze.elm.ratio :refer [ratio]] + [blaze.elm.value-set :as value-set] + [blaze.fhir.spec.type :as type] + [blaze.terminology-service :as ts])) (defn- find-code-system-def "Returns the code-system-def with `name` from `library` or nil if not found." @@ -113,5 +117,31 @@ ;; Not needed because it's not an expression. ;; 3.12. ValueSetRef -;; -;; TODO +(defn- find-value-set-def + "Returns the value-set-def with `name` from `library` or nil if not found." + {:arglists '([library name])} + [{{value-set-defs :def} :valueSets} name] + (some #(when (= name (:name %)) %) value-set-defs)) + +(defn- to-code [{:keys [system code]}] + (value-set/->Code (type/value system) (type/value code))) + +(defn- value-set [{{:keys [contains]} :expansion}] + (into #{} (map to-code) contains)) + +(defn- expand-value-set [terminology-service request] + (try + @(ts/expand-value-set terminology-service request) + (catch Exception e + (throw (ex-cause e))))) + +(defmethod core/compile* :elm.compiler.type/value-set-ref + [{:keys [library terminology-service]} {:keys [name]}] + (when-let [{:keys [id]} (find-value-set-def library name)] + (reify-expr core/Expression + (-optimize [_ db] + (value-set (expand-value-set terminology-service {:db db :url id}))) + (-eval [_ _ _ _] + (throw-anom (ba/fault "Can't eval value-set expression without optimization."))) + (-form [_] + (list 'value-set id))))) diff --git a/modules/cql/src/blaze/elm/compiler/core.clj b/modules/cql/src/blaze/elm/compiler/core.clj index eabac97c1..706b035f8 100644 --- a/modules/cql/src/blaze/elm/compiler/core.clj +++ b/modules/cql/src/blaze/elm/compiler/core.clj @@ -5,7 +5,7 @@ [blaze.fhir.spec.type.system :as system] [clojure.string :as str]) (:import - [clojure.lang IReduceInit] + [clojure.lang IPersistentSet IReduceInit] [java.time.temporal ChronoUnit])) (set! *warn-on-reflection* true) @@ -200,7 +200,25 @@ (-eval [expr _ _ _] expr) (-form [expr] - (mapv -form expr))) + (mapv -form expr)) + + IPersistentSet + (-static [_] + true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) + (-optimize [expr _] + expr) + (-eval [expr _ _ _] + expr) + (-form [expr] + `(~'set ~@(mapv -form expr)))) (defmulti compile* "Compiles `expression` in `context`. diff --git a/modules/cql/src/blaze/elm/compiler/external_data.clj b/modules/cql/src/blaze/elm/compiler/external_data.clj index 29e086b00..a72e5ad3d 100644 --- a/modules/cql/src/blaze/elm/compiler/external_data.clj +++ b/modules/cql/src/blaze/elm/compiler/external_data.clj @@ -44,6 +44,8 @@ data-type clauses)] (reify-expr core/Expression (-optimize [expr db] + ;; if there is no resource, regardless of the individual patient, + ;; available, just return an empty list for further optimizations (if (coll/empty? (d/execute-query db type-query)) [] expr)) diff --git a/modules/cql/src/blaze/elm/compiler/library.clj b/modules/cql/src/blaze/elm/compiler/library.clj index a72e4eda3..8d186d81f 100644 --- a/modules/cql/src/blaze/elm/compiler/library.clj +++ b/modules/cql/src/blaze/elm/compiler/library.clj @@ -97,9 +97,9 @@ :function-defs and :parameter-default-values. There are currently no options." - [node library opts] + [context library opts] (let [library (normalizer/normalize-library library) - context (assoc opts :node node :library library)] + context (merge opts context {:library library})] (when-ok [{:keys [function-defs] :as context} (compile-function-defs context library) expression-defs (expression-defs context library) expression-defs (resolve-refs (unfiltered-expr-names expression-defs) expression-defs) diff --git a/modules/cql/src/blaze/elm/compiler/library/spec.clj b/modules/cql/src/blaze/elm/compiler/library/spec.clj index fa7ea540e..61a87a755 100644 --- a/modules/cql/src/blaze/elm/compiler/library/spec.clj +++ b/modules/cql/src/blaze/elm/compiler/library/spec.clj @@ -5,6 +5,7 @@ [blaze.elm.compiler.function-def :as-alias function-def] [blaze.elm.compiler.spec] [blaze.fhir.spec.spec] + [blaze.terminology-service.spec] [clojure.spec.alpha :as s])) (s/def ::expression-def/name @@ -40,5 +41,8 @@ (s/def ::c/library (s/keys :req-un [::c/expression-defs ::c/function-defs ::c/parameter-default-values])) +(s/def ::c/context + (s/keys :req-un [:blaze.db/node :blaze/terminology-service])) + (s/def ::c/options map?) diff --git a/modules/cql/src/blaze/elm/compiler/library_spec.clj b/modules/cql/src/blaze/elm/compiler/library_spec.clj index 2824106a3..a359df3c2 100644 --- a/modules/cql/src/blaze/elm/compiler/library_spec.clj +++ b/modules/cql/src/blaze/elm/compiler/library_spec.clj @@ -11,7 +11,7 @@ [cognitect.anomalies :as-alias anom])) (s/fdef library/compile-library - :args (s/cat :node :blaze.db/node :library :elm/library :opts ::c/options) + :args (s/cat :context ::c/context :library :elm/library :opts ::c/options) :ret (s/or :library ::c/library :anomaly ::anom/anomaly)) (s/fdef library/resolve-all-refs diff --git a/modules/cql/src/blaze/elm/concept.clj b/modules/cql/src/blaze/elm/concept.clj index 510c834a5..cde007f8d 100644 --- a/modules/cql/src/blaze/elm/concept.clj +++ b/modules/cql/src/blaze/elm/concept.clj @@ -33,6 +33,9 @@ (-form [_] `(~'concept ~@(map core/-form codes)))) +(defn concept? [x] + (instance? Concept x)) + (defn concept "Returns a CQL concept" [codes] diff --git a/modules/cql/src/blaze/elm/spec.clj b/modules/cql/src/blaze/elm/spec.clj index 2a7eac72a..0b9e7e65b 100644 --- a/modules/cql/src/blaze/elm/spec.clj +++ b/modules/cql/src/blaze/elm/spec.clj @@ -208,7 +208,7 @@ ;; 3. Clinical Values (s/def :elm/code-system-ref - (s/keys :opt-un [:elm/name :elm/libraryName])) + (s/keys :req-un [:elm/name] :opt-un [:elm/libraryName])) ;; 3.1. Code (s/def :elm.code/system @@ -237,7 +237,7 @@ ;; 3.3. CodeRef (s/def :elm/code-ref - (s/keys :opt-un [:elm/name :elm/libraryName])) + (s/keys :req-un [:elm/name] :opt-un [:elm/libraryName])) (defmethod expression :elm.spec.type/code-ref [_] :elm/code-ref) @@ -271,7 +271,7 @@ ;; 3.8. ConceptRef (s/def :elm/concept-ref - (s/keys :opt-un [:elm/name :elm/libraryName])) + (s/keys :req-un [:elm/name] :opt-un [:elm/libraryName])) (defmethod expression :elm.spec.type/concept-ref [_] :elm/concept-ref) @@ -362,6 +362,25 @@ (defmethod expression :elm.spec.type/ratio [_] :elm/ratio) +;; 3.11. ValueSetDef +(s/def :elm.value-set-def/codeSystem + (s/coll-of :elm/code-system-ref)) + +(s/def :elm/value-set-def + (s/keys :req-un [:elm/name :elm/id] + :opt-un [:elm/version :elm.value-set-def/codeSystem])) + +;; 3.12. ValueSetRef +(s/def :elm.value-set-ref/preserve + boolean?) + +(s/def :elm/value-set-ref + (s/keys :req-un [:elm/name] + :opt-un [:elm/libraryName :elm.value-set-ref/preserve])) + +(defmethod expression :elm.spec.type/value-set-ref [_] + :elm/value-set-ref) + ;; 4. Type Specifiers ;; 4.1. TypeSpecifier @@ -392,7 +411,8 @@ :elm/type-specifier) (s/def :elm/tuple-element-definition - (s/keys :req-un [:elm/name] :opt-un [:elm.tuple-element-definition/type :elm/elementType])) + (s/keys :req-un [:elm/name] + :opt-un [:elm.tuple-element-definition/type :elm/elementType])) (s/def :elm.tuple-type-specifier/element (s/coll-of :elm/tuple-element-definition)) @@ -474,7 +494,7 @@ ;; 7.2. ParameterRef (defmethod expression :elm.spec.type/parameter-ref [_] - (s/keys :opt-un [:elm/name :elm/libraryName])) + (s/keys :req-un [:elm/name] :opt-un [:elm/libraryName])) ;; 8. Expressions @@ -1479,3 +1499,18 @@ (defmethod expression :elm.spec.type/calculate-age-at [_] (s/keys :req-un [:elm.binary-expression/operand] :opt-un [:elm.date/precision])) + +;; 23.8. InValueSet +(s/def :elm.in-value-set/code + :elm/expression) + +(s/def :elm.in-value-set/valueset + :elm/value-set-ref) + +(s/def :elm.in-value-set/valuesetExpression + :elm/expression) + +(defmethod expression :elm.spec.type/in-value-set [_] + (s/keys :req-un [:elm.in-value-set/code] + :opt-un [:elm.in-value-set/valueset + :elm.in-value-set/valuesetExpression])) diff --git a/modules/cql/src/blaze/elm/value_set.clj b/modules/cql/src/blaze/elm/value_set.clj new file mode 100644 index 000000000..006b3df3c --- /dev/null +++ b/modules/cql/src/blaze/elm/value_set.clj @@ -0,0 +1,27 @@ +(ns blaze.elm.value-set + (:require + [blaze.elm.compiler.core :as core])) + +;; A code without it's version and display so that it's Clojure equal operation +;; will be the CQL equivalence operation. +(defrecord Code [system code] + core/Expression + (-static [_] + true) + (-attach-cache [expr _] + [(fn [] [expr])]) + (-patient-count [_] + nil) + (-resolve-refs [expr _] + expr) + (-resolve-params [expr _] + expr) + (-optimize [expr _] + expr) + (-eval [this _ _ _] + this) + (-form [_] + `(~'code ~system ~code))) + +(defn from-code [{:keys [system code]}] + (->Code system code)) diff --git a/modules/cql/test-perf/blaze/elm/compiler/library_test_perf.clj b/modules/cql/test-perf/blaze/elm/compiler/library_test_perf.clj index e25359d8d..c252c53f1 100644 --- a/modules/cql/test-perf/blaze/elm/compiler/library_test_perf.clj +++ b/modules/cql/test-perf/blaze/elm/compiler/library_test_perf.clj @@ -6,14 +6,19 @@ [blaze.elm.compiler.library-spec] [blaze.fhir.spec.type.system] [blaze.module.test-util :refer [with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] [clojure.test :refer [deftest]] [criterium.core :as criterium] [taoensso.timbre :as log])) (log/set-min-level! :info) +(def config + (assoc mem-node-config ::ts/local {})) + (deftest compile-test (let [library (t/translate "library \"antidiabetika\"\nusing FHIR version '4.0.0'\ninclude FHIRHelpers version '4.0.0'\n\ncodesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc'\n\ncontext Unfiltered\n\ndefine GliclazidA10BB09Ref:\n from [Medication: Code 'A10BB09' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin degludec und Insulin aspartA10AD06Ref\":\n from [Medication: Code 'A10AD06' from atc] M\n return 'Medication/' + M.id\n\ndefine SulfonylharnstoffeA10BBRef:\n from [Medication: Code 'A10BB' from atc] M\n return 'Medication/' + M.id\n\ndefine TolbutamidA10BB03Ref:\n from [Medication: Code 'A10BB03' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insuline und Analoga zur Injektion, intermediär oder lang wirkend kombiniert mit schnell wirkendA10ADRef\":\n from [Medication: Code 'A10AD' from atc] M\n return 'Medication/' + M.id\n\ndefine LiraglutidA10BJ02Ref:\n from [Medication: Code 'A10BJ02' from atc] M\n return 'Medication/' + M.id\n\ndefine EvogliptinA10BH07Ref:\n from [Medication: Code 'A10BH07' from atc] M\n return 'Medication/' + M.id\n\ndefine GlibornuridA10BB04Ref:\n from [Medication: Code 'A10BB04' from atc] M\n return 'Medication/' + M.id\n\ndefine ErtugliflozinA10BK04Ref:\n from [Medication: Code 'A10BK04' from atc] M\n return 'Medication/' + M.id\n\ndefine KombinationenA10AE30Ref:\n from [Medication: Code 'A10AE30' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und SulfonylharnstoffeA10BD02Ref\":\n from [Medication: Code 'A10BD02' from atc] M\n return 'Medication/' + M.id\n\ndefine AcetohexamidA10BB31Ref:\n from [Medication: Code 'A10BB31' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und AcarboseA10BD17Ref\":\n from [Medication: Code 'A10BD17' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Glimepirid und RosiglitazonA10BD04Ref\":\n from [Medication: Code 'A10BD04' from atc] M\n return 'Medication/' + M.id\n\ndefine ChlorpropamidA10BB02Ref:\n from [Medication: Code 'A10BB02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insuline und Analoga zur Injektion, schnell wirkendA10ABRef\":\n from [Medication: Code 'A10AB' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (human)A10AC01Ref\":\n from [Medication: Code 'A10AC01' from atc] M\n return 'Medication/' + M.id\n\ndefine VogliboseA10BF03Ref:\n from [Medication: Code 'A10BF03' from atc] M\n return 'Medication/' + M.id\n\ndefine GlimepiridA10BB12Ref:\n from [Medication: Code 'A10BB12' from atc] M\n return 'Medication/' + M.id\n\ndefine DapagliflozinA10BK01Ref:\n from [Medication: Code 'A10BK01' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und DapagliflozinA10BD15Ref\":\n from [Medication: Code 'A10BD15' from atc] M\n return 'Medication/' + M.id\n\ndefine KombinationenA10AB30Ref:\n from [Medication: Code 'A10AB30' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin degludec und LiraglutidA10AE56Ref\":\n from [Medication: Code 'A10AE56' from atc] M\n return 'Medication/' + M.id\n\ndefine CanagliflozinA10BK02Ref:\n from [Medication: Code 'A10BK02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und AlogliptinA10BD13Ref\":\n from [Medication: Code 'A10BD13' from atc] M\n return 'Medication/' + M.id\n\ndefine ExenatidA10BJ01Ref:\n from [Medication: Code 'A10BJ01' from atc] M\n return 'Medication/' + M.id\n\ndefine GliquidonA10BB08Ref:\n from [Medication: Code 'A10BB08' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin glargin und LixisenatidA10AE54Ref\":\n from [Medication: Code 'A10AE54' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin detemirA10AE05Ref\":\n from [Medication: Code 'A10AE05' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin, Saxagliptin und DapagliflozinA10BD25Ref\":\n from [Medication: Code 'A10BD25' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Rind)A10AB02Ref\":\n from [Medication: Code 'A10AB02' from atc] M\n return 'Medication/' + M.id\n\ndefine VildagliptinA10BH02Ref:\n from [Medication: Code 'A10BH02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Phenformin und SulfonylharnstoffeA10BD01Ref\":\n from [Medication: Code 'A10BD01' from atc] M\n return 'Medication/' + M.id\n\ndefine KombinationenA10AC30Ref:\n from [Medication: Code 'A10AC30' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und ErtugliflozinA10BD23Ref\":\n from [Medication: Code 'A10BD23' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und SaxagliptinA10BD10Ref\":\n from [Medication: Code 'A10BD10' from atc] M\n return 'Medication/' + M.id\n\ndefine SaxagliptinA10BH03Ref:\n from [Medication: Code 'A10BH03' from atc] M\n return 'Medication/' + M.id\n\ndefine LinagliptinA10BH05Ref:\n from [Medication: Code 'A10BH05' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Guar-Mehl*A10BX01Ref\":\n from [Medication: Code 'A10BX01' from atc] M\n return 'Medication/' + M.id\n\ndefine RosiglitazonA10BG02Ref:\n from [Medication: Code 'A10BG02' from atc] M\n return 'Medication/' + M.id\n\ndefine GlisoxepidA10BB11Ref:\n from [Medication: Code 'A10BB11' from atc] M\n return 'Medication/' + M.id\n\ndefine GlibenclamidA10BB01Ref:\n from [Medication: Code 'A10BB01' from atc] M\n return 'Medication/' + M.id\n\ndefine TeneligliptinA10BH08Ref:\n from [Medication: Code 'A10BH08' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin glulisinA10AB06Ref\":\n from [Medication: Code 'A10AB06' from atc] M\n return 'Medication/' + M.id\n\ndefine AldosereduktasehemmerA10XARef:\n from [Medication: Code 'A10XA' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin aspartA10AB05Ref\":\n from [Medication: Code 'A10AB05' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin glarginA10AE04Ref\":\n from [Medication: Code 'A10AE04' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insuline Und AnalogaA10ARef\":\n from [Medication: Code 'A10A' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Schwein)A10AE03Ref\":\n from [Medication: Code 'A10AE03' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (human)A10AB01Ref\":\n from [Medication: Code 'A10AB01' from atc] M\n return 'Medication/' + M.id\n\ndefine MetforminA10BA02Ref:\n from [Medication: Code 'A10BA02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Sulfonamide (heterozyklisch)A10BCRef\":\n from [Medication: Code 'A10BC' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Pioglitazon und SitagliptinA10BD12Ref\":\n from [Medication: Code 'A10BD12' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Homöopathische und anthroposophische AntidiabetikaA10XHRef\":\n from [Medication: Code 'A10XH' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und EvogliptinA10BD22Ref\":\n from [Medication: Code 'A10BD22' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (human)A10AE01Ref\":\n from [Medication: Code 'A10AE01' from atc] M\n return 'Medication/' + M.id\n\ndefine SitagliptinA10BH01Ref:\n from [Medication: Code 'A10BH01' from atc] M\n return 'Medication/' + M.id\n\ndefine CopalchirindenextraktA10XP02Ref:\n from [Medication: Code 'A10XP02' from atc] M\n return 'Medication/' + M.id\n\ndefine ImegliminA10BX15Ref:\n from [Medication: Code 'A10BX15' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und GemigliptinA10BD18Ref\":\n from [Medication: Code 'A10BD18' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Dipeptidyl-Peptidase-4-(DPP-4)-InhibitorenA10BHRef\":\n from [Medication: Code 'A10BH' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin lisproA10AC04Ref\":\n from [Medication: Code 'A10AC04' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und EmpagliflozinA10BD20Ref\":\n from [Medication: Code 'A10BD20' from atc] M\n return 'Medication/' + M.id\n\ndefine GlipizidA10BB07Ref:\n from [Medication: Code 'A10BB07' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Sitagliptin und SimvastatinA10BH51Ref\":\n from [Medication: Code 'A10BH51' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und RosiglitazonA10BD03Ref\":\n from [Medication: Code 'A10BD03' from atc] M\n return 'Medication/' + M.id\n\ndefine PhenforminA10BA01Ref:\n from [Medication: Code 'A10BA01' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Gemigliptin und RosuvastatinA10BH52Ref\":\n from [Medication: Code 'A10BH52' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Rind)A10AD02Ref\":\n from [Medication: Code 'A10AD02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Schwein)A10AC03Ref\":\n from [Medication: Code 'A10AC03' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insuline und Analoga zur InhalationA10AFRef\":\n from [Medication: Code 'A10AF' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Linagliptin und EmpagliflozinA10BD19Ref\":\n from [Medication: Code 'A10BD19' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insuline und Analoga zur Injektion, lang wirkendA10AERef\":\n from [Medication: Code 'A10AE' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und GlibenclamidA10BD31Ref\":\n from [Medication: Code 'A10BD31' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Sitagliptin und ErtugliflozinA10BD24Ref\":\n from [Medication: Code 'A10BD24' from atc] M\n return 'Medication/' + M.id\n\ndefine BenfluorexA10BX06Ref:\n from [Medication: Code 'A10BX06' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und VildagliptinA10BD08Ref\":\n from [Medication: Code 'A10BD08' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Glucagon-like-Peptid-1-(GLP-1)-RezeptoragonistenA10BJRef\":\n from [Medication: Code 'A10BJ' from atc] M\n return 'Medication/' + M.id\n\ndefine TroglitazonA10BG01Ref:\n from [Medication: Code 'A10BG01' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin aspartA10AD05Ref\":\n from [Medication: Code 'A10AD05' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Andere AntidiabetikaA10XRef\":\n from [Medication: Code 'A10X' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (human)A10AD01Ref\":\n from [Medication: Code 'A10AD01' from atc] M\n return 'Medication/' + M.id\n\ndefine AntidiabetikaA10Ref:\n from [Medication: Code 'A10' from atc] M\n return 'Medication/' + M.id\n\ndefine EmpagliflozinA10BK03Ref:\n from [Medication: Code 'A10BK03' from atc] M\n return 'Medication/' + M.id\n\ndefine PioglitazonA10BG03Ref:\n from [Medication: Code 'A10BG03' from atc] M\n return 'Medication/' + M.id\n\ndefine CarbutamidA10BB06Ref:\n from [Medication: Code 'A10BB06' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin lisproA10AB04Ref\":\n from [Medication: Code 'A10AB04' from atc] M\n return 'Medication/' + M.id\n\ndefine MiglitolA10BF02Ref:\n from [Medication: Code 'A10BF02' from atc] M\n return 'Medication/' + M.id\n\ndefine RepaglinidA10BX02Ref:\n from [Medication: Code 'A10BX02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und SitagliptinA10BD07Ref\":\n from [Medication: Code 'A10BD07' from atc] M\n return 'Medication/' + M.id\n\ndefine GemigliptinA10BH06Ref:\n from [Medication: Code 'A10BH06' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Rind)A10AC02Ref\":\n from [Medication: Code 'A10AC02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und RepaglinidA10BD14Ref\":\n from [Medication: Code 'A10BD14' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Schwein)A10AB03Ref\":\n from [Medication: Code 'A10AB03' from atc] M\n return 'Medication/' + M.id\n\ndefine KombinationenA10AD30Ref:\n from [Medication: Code 'A10AD30' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und CanagliflozinA10BD16Ref\":\n from [Medication: Code 'A10BD16' from atc] M\n return 'Medication/' + M.id\n\ndefine SemaglutidA10BJ06Ref:\n from [Medication: Code 'A10BJ06' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Thiazolidindione (Glitazone)A10BGRef\":\n from [Medication: Code 'A10BG' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Kombinationen oraler AntidiabetikaA10BDRef\":\n from [Medication: Code 'A10BD' from atc] M\n return 'Medication/' + M.id\n\ndefine IpragliflozinA10BK05Ref:\n from [Medication: Code 'A10BK05' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Guar-MehlA10XP01Ref\":\n from [Medication: Code 'A10XP01' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Pioglitazon und AlogliptinA10BD09Ref\":\n from [Medication: Code 'A10BD09' from atc] M\n return 'Medication/' + M.id\n\ndefine AcarboseA10BF01Ref:\n from [Medication: Code 'A10BF01' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Saxagliptin und DapagliflozinA10BD21Ref\":\n from [Medication: Code 'A10BD21' from atc] M\n return 'Medication/' + M.id\n\ndefine AlbiglutidA10BJ04Ref:\n from [Medication: Code 'A10BJ04' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Pflanzliche AntidiabetikaA10XPRef\":\n from [Medication: Code 'A10XP' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insuline und Analoga zur Injektion, intermediär wirkendA10ACRef\":\n from [Medication: Code 'A10AC' from atc] M\n return 'Medication/' + M.id\n\ndefine TolazamidA10BB05Ref:\n from [Medication: Code 'A10BB05' from atc] M\n return 'Medication/' + M.id\n\ndefine SotagliflozinA10BK06Ref:\n from [Medication: Code 'A10BK06' from atc] M\n return 'Medication/' + M.id\n\ndefine TolrestatA10XA01Ref:\n from [Medication: Code 'A10XA01' from atc] M\n return 'Medication/' + M.id\n\ndefine BuforminA10BA03Ref:\n from [Medication: Code 'A10BA03' from atc] M\n return 'Medication/' + M.id\n\ndefine BiguanideA10BARef:\n from [Medication: Code 'A10BA' from atc] M\n return 'Medication/' + M.id\n\ndefine LuseogliflozinA10BK07Ref:\n from [Medication: Code 'A10BK07' from atc] M\n return 'Medication/' + M.id\n\ndefine AlogliptinA10BH04Ref:\n from [Medication: Code 'A10BH04' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (human)A10AF01Ref\":\n from [Medication: Code 'A10AF01' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und PioglitazonA10BD05Ref\":\n from [Medication: Code 'A10BD05' from atc] M\n return 'Medication/' + M.id\n\ndefine MitiglinidA10BX08Ref:\n from [Medication: Code 'A10BX08' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin degludecA10AE06Ref\":\n from [Medication: Code 'A10AE06' from atc] M\n return 'Medication/' + M.id\n\ndefine BeinaglutidA10BJ07Ref:\n from [Medication: Code 'A10BJ07' from atc] M\n return 'Medication/' + M.id\n\ndefine LobeglitazonA10BG04Ref:\n from [Medication: Code 'A10BG04' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und LobeglitazonA10BD26Ref\":\n from [Medication: Code 'A10BD26' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin lisproA10AD04Ref\":\n from [Medication: Code 'A10AD04' from atc] M\n return 'Medication/' + M.id\n\ndefine DulaglutidA10BJ05Ref:\n from [Medication: Code 'A10BJ05' from atc] M\n return 'Medication/' + M.id\n\ndefine KombinationenA10XP30Ref:\n from [Medication: Code 'A10XP30' from atc] M\n return 'Medication/' + M.id\n\ndefine NateglinidA10BX03Ref:\n from [Medication: Code 'A10BX03' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Rind)A10AE02Ref\":\n from [Medication: Code 'A10AE02' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Insulin (Schwein)A10AD03Ref\":\n from [Medication: Code 'A10AD03' from atc] M\n return 'Medication/' + M.id\n\ndefine PramlintidA10BX05Ref:\n from [Medication: Code 'A10BX05' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Natrium-Glucose-Cotransporter-2-(SGLT2)-InhibitorenA10BKRef\":\n from [Medication: Code 'A10BK' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Antidiabetika, Exkl. InsulineA10BRef\":\n from [Medication: Code 'A10B' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin, Linagliptin und EmpagliflozinA10BD27Ref\":\n from [Medication: Code 'A10BD27' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Andere Antidiabetika, exkl. InsulineA10BXRef\":\n from [Medication: Code 'A10BX' from atc] M\n return 'Medication/' + M.id\n\ndefine KombinationenA10XH20Ref:\n from [Medication: Code 'A10XH20' from atc] M\n return 'Medication/' + M.id\n\ndefine MetahexamidA10BB10Ref:\n from [Medication: Code 'A10BB10' from atc] M\n return 'Medication/' + M.id\n\ndefine GlymidinA10BC01Ref:\n from [Medication: Code 'A10BC01' from atc] M\n return 'Medication/' + M.id\n\ndefine LixisenatidA10BJ03Ref:\n from [Medication: Code 'A10BJ03' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Glimepirid und PioglitazonA10BD06Ref\":\n from [Medication: Code 'A10BD06' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Alpha-GlukosidasehemmerA10BFRef\":\n from [Medication: Code 'A10BF' from atc] M\n return 'Medication/' + M.id\n\ndefine \"Metformin und LinagliptinA10BD11Ref\":\n from [Medication: Code 'A10BD11' from atc] M\n return 'Medication/' + M.id\n\ncontext Patient\n\ndefine InInitialPopulation:\n exists (from [MedicationAdministration] M\n where M.medication.reference in AntidiabetikaA10Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Antidiabetika, Exkl. InsulineA10BRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Alpha-GlukosidasehemmerA10BFRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in AcarboseA10BF01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in MiglitolA10BF02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in VogliboseA10BF03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in BiguanideA10BARef) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in BuforminA10BA03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in MetforminA10BA02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in PhenforminA10BA01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Dipeptidyl-Peptidase-4-(DPP-4)-InhibitorenA10BHRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in AlogliptinA10BH04Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in EvogliptinA10BH07Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GemigliptinA10BH06Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Gemigliptin und RosuvastatinA10BH52Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in LinagliptinA10BH05Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in SaxagliptinA10BH03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in SitagliptinA10BH01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Sitagliptin und SimvastatinA10BH51Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in TeneligliptinA10BH08Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in VildagliptinA10BH02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Glucagon-like-Peptid-1-(GLP-1)-RezeptoragonistenA10BJRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in AlbiglutidA10BJ04Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in BeinaglutidA10BJ07Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in DulaglutidA10BJ05Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in ExenatidA10BJ01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in LiraglutidA10BJ02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in LixisenatidA10BJ03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in SemaglutidA10BJ06Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Kombinationen oraler AntidiabetikaA10BDRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Glimepirid und PioglitazonA10BD06Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Glimepirid und RosiglitazonA10BD04Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Linagliptin und EmpagliflozinA10BD19Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und AcarboseA10BD17Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und AlogliptinA10BD13Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und CanagliflozinA10BD16Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und DapagliflozinA10BD15Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und EmpagliflozinA10BD20Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und ErtugliflozinA10BD23Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und EvogliptinA10BD22Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und GemigliptinA10BD18Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und GlibenclamidA10BD31Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und LinagliptinA10BD11Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und LobeglitazonA10BD26Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und PioglitazonA10BD05Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und RepaglinidA10BD14Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und RosiglitazonA10BD03Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und SaxagliptinA10BD10Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und SitagliptinA10BD07Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und SulfonylharnstoffeA10BD02Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin und VildagliptinA10BD08Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin, Linagliptin und EmpagliflozinA10BD27Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Metformin, Saxagliptin und DapagliflozinA10BD25Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Phenformin und SulfonylharnstoffeA10BD01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Pioglitazon und AlogliptinA10BD09Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Pioglitazon und SitagliptinA10BD12Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Saxagliptin und DapagliflozinA10BD21Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Sitagliptin und ErtugliflozinA10BD24Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Natrium-Glucose-Cotransporter-2-(SGLT2)-InhibitorenA10BKRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in CanagliflozinA10BK02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in DapagliflozinA10BK01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in EmpagliflozinA10BK03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in ErtugliflozinA10BK04Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in IpragliflozinA10BK05Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in LuseogliflozinA10BK07Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in SotagliflozinA10BK06Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Sulfonamide (heterozyklisch)A10BCRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GlymidinA10BC01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in SulfonylharnstoffeA10BBRef) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in AcetohexamidA10BB31Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in CarbutamidA10BB06Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in ChlorpropamidA10BB02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GlibenclamidA10BB01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GlibornuridA10BB04Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GliclazidA10BB09Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GlimepiridA10BB12Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GlipizidA10BB07Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GliquidonA10BB08Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in GlisoxepidA10BB11Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in MetahexamidA10BB10Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in TolazamidA10BB05Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in TolbutamidA10BB03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Thiazolidindione (Glitazone)A10BGRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in LobeglitazonA10BG04Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in PioglitazonA10BG03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in RosiglitazonA10BG02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in TroglitazonA10BG01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Andere Antidiabetika, exkl. InsulineA10BXRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in BenfluorexA10BX06Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Guar-Mehl*A10BX01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in ImegliminA10BX15Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in MitiglinidA10BX08Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in NateglinidA10BX03Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in PramlintidA10BX05Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in RepaglinidA10BX02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insuline Und AnalogaA10ARef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insuline und Analoga zur InhalationA10AFRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (human)A10AF01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insuline und Analoga zur Injektion, intermediär oder lang wirkend kombiniert mit schnell wirkendA10ADRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (human)A10AD01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Rind)A10AD02Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Schwein)A10AD03Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin aspartA10AD05Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin degludec und Insulin aspartA10AD06Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin lisproA10AD04Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in KombinationenA10AD30Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insuline und Analoga zur Injektion, intermediär wirkendA10ACRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (human)A10AC01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Rind)A10AC02Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Schwein)A10AC03Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin lisproA10AC04Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in KombinationenA10AC30Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insuline und Analoga zur Injektion, lang wirkendA10AERef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (human)A10AE01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Rind)A10AE02Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Schwein)A10AE03Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin degludecA10AE06Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin degludec und LiraglutidA10AE56Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin detemirA10AE05Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin glarginA10AE04Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin glargin und LixisenatidA10AE54Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in KombinationenA10AE30Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insuline und Analoga zur Injektion, schnell wirkendA10ABRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (human)A10AB01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Rind)A10AB02Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin (Schwein)A10AB03Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin aspartA10AB05Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin glulisinA10AB06Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Insulin lisproA10AB04Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in KombinationenA10AB30Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Andere AntidiabetikaA10XRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in AldosereduktasehemmerA10XARef) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in TolrestatA10XA01Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Homöopathische und anthroposophische AntidiabetikaA10XHRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in KombinationenA10XH20Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Pflanzliche AntidiabetikaA10XPRef\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in CopalchirindenextraktA10XP02Ref) or\n exists (from [MedicationAdministration] M\n where M.medication.reference in \"Guar-MehlA10XP01Ref\") or\n exists (from [MedicationAdministration] M\n where M.medication.reference in KombinationenA10XP30Ref)\n")] - (with-system [{:blaze.db/keys [node]} mem-node-config] + (with-system [{:blaze.db/keys [node] ::ts/keys [local]} config] ;; 1.7 ms / 15 µs - Macbook Pro M1 Pro, Oracle OpenJDK 17.0.2 - (criterium/quick-bench (library/compile-library node library {}))))) + (criterium/quick-bench (library/compile-library {:node node :terminology-service local} library {}))))) diff --git a/modules/cql/test/blaze/cql/translator_test.clj b/modules/cql/test/blaze/cql/translator_test.clj index fe8c3be49..8c95160c3 100644 --- a/modules/cql/test/blaze/cql/translator_test.clj +++ b/modules/cql/test/blaze/cql/translator_test.clj @@ -108,6 +108,30 @@ [0 :expression :operand :where :operand 0 :operand 0 :type] := "As" [0 :expression :operand :where :operand 0 :operand 0 :asType] := "{http://hl7.org/fhir}Period")) + (testing "Valueset" + (given-translation + "library Test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + valueset \"Female Administrative Sex\": 'urn:oid:2.16.840.1.113883.3.560.100.2' + context Patient + define \"PatientIsFemale\": Patient.gender in \"Female Administrative Sex\"" + [0 :name] := "Patient" + [0 :context] := "Patient" + [0 :expression :type] := "SingletonFrom" + [0 :expression :operand :type] := "Retrieve" + [0 :expression :operand :dataType] := "{http://hl7.org/fhir}Patient" + [1 :name] := "PatientIsFemale" + [1 :context] := "Patient" + [1 :expression :type] := "InValueSet" + [1 :expression :resultTypeName] := "{urn:hl7-org:elm-types:r1}Boolean" + [1 :expression :valueset :name] := "Female Administrative Sex" + [1 :expression :code :type] := "FunctionRef" + [1 :expression :code :name] := "ToString" + [1 :expression :code :operand 0 :type] := "Property" + [1 :expression :code :operand 0 :source :type] := "ExpressionRef" + [1 :expression :code :operand 0 :source :name] := "Patient")) + (testing "Returns a valid :elm/library" (are [cql] (s/valid? :elm/library (translate cql)) "library Test diff --git a/modules/cql/test/blaze/elm/compiler/clinical_operators/impl_test.clj b/modules/cql/test/blaze/elm/compiler/clinical_operators/impl_test.clj new file mode 100644 index 000000000..3980dddb9 --- /dev/null +++ b/modules/cql/test/blaze/elm/compiler/clinical_operators/impl_test.clj @@ -0,0 +1,30 @@ +(ns blaze.elm.compiler.clinical-operators.impl-test + (:require + [blaze.elm.code :as code] + [blaze.elm.compiler.clinical-operators.impl :as impl] + [blaze.elm.value-set :as value-set] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [are deftest is testing]])) + +(st/instrument) + +(test/use-fixtures :each tu/fixture) + +(deftest contains-any-test + (testing "no code" + (is (false? (impl/contains-any? #{} nil)))) + + (testing "one code" + (let [code (code/code "system-125341" "version-131455" "code-131458")] + (are [value-set pred] (pred (impl/contains-any? value-set [code])) + #{(value-set/->Code "system-125341" "code-131337")} false? + #{(value-set/->Code "system-125341" "code-131458")} true?))) + + (testing "two codes" + (let [code-1 (code/code "system-125341" "version-131455" "code-131458") + code-2 (code/code "system-131726" "version-131455" "code-131653")] + (are [value-set pred] (pred (impl/contains-any? value-set [code-1 code-2])) + #{(value-set/->Code "system-125341" "code-131337")} false? + #{(value-set/->Code "system-125341" "code-131458")} true? + #{(value-set/->Code "system-131726" "code-131653")} true?)))) diff --git a/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj b/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj index 77dce734a..3baea759e 100644 --- a/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj +++ b/modules/cql/test/blaze/elm/compiler/clinical_operators_test.clj @@ -4,15 +4,23 @@ Section numbers are according to https://cql.hl7.org/04-logicalspecification.html." (:require + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config with-system-data]] [blaze.elm.compiler :as c] [blaze.elm.compiler.clinical-operators] [blaze.elm.compiler.core :as core] [blaze.elm.compiler.core-spec] - [blaze.elm.compiler.test-util :as ctu] + [blaze.elm.compiler.test-util :as ctu :refer [has-form]] + [blaze.elm.expression :as expr] + [blaze.elm.expression-spec] [blaze.elm.literal :as elm] [blaze.elm.literal-spec] + [blaze.terminology-service :as ts] + [blaze.terminology-service-spec] + [blaze.terminology-service.local] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [are deftest testing]])) + [clojure.test :as test :refer [are deftest is testing]] + [java-time.api :as time])) (st/instrument) (ctu/instrument-compile) @@ -93,7 +101,150 @@ ;; TODO 23.7. InCodeSystem -;; TODO 23.8. InValueSet +;; 23.8. InValueSet +;; +;; The InValueSet operator returns true if the given code is in the given value +;; set. +;; +;; The first argument is expected to be a String, Code, or Concept. +;; +;; The second argument is statically a ValueSetRef. This allows for both static +;; analysis of the value set references within an artifact, as well as the +;; implementation of value set membership by the target environment as a service +;; call to a terminology server, if desired. +;; +;; The third argument is expected to be a ValueSet, allowing references to value +;; sets to be preserved as references. +(def ^:private config + (assoc mem-node-config ::ts/local {})) + +(defn- eval-context [db] + {:db db :now (time/offset-date-time)}) + +(deftest in-value-set-test + (testing "Code" + (with-system-data [{:blaze.db/keys [node] terminology-service ::ts/local} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-105600"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (let [context + {:library + {:codeSystems + {:def [{:name "sys-def-115852" :id "system-115910"}]} + :valueSets + {:def + [{:name "value-set-def-135520" + :id "value-set-135750"}]}} + :terminology-service terminology-service} + elm #elm/in-value-set [#elm/code ["sys-def-115852" "code-115927"] + #elm/value-set-ref "value-set-def-135520"] + expr (c/compile context elm) + db (d/db node) + expr (c/optimize expr db)] + + (testing "eval" + (is (true? (expr/eval (eval-context db) expr nil)))) + + (testing "form" + (has-form expr + '(in-value-set + (code "system-115910" nil "code-115927") + (set (code "system-115910" "code-115927") + (code "system-115910" "code-105600")))))))) + + (testing "String" + (with-system-data [{:blaze.db/keys [node] terminology-service ::ts/local} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-105600"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (let [context + {:library + {:valueSets + {:def + [{:name "value-set-def-135520" + :id "value-set-135750"}]}} + :terminology-service terminology-service} + elm #elm/in-value-set [#elm/string "code-115927" + #elm/value-set-ref "value-set-def-135520"] + expr (c/compile context elm) + db (d/db node) + expr (c/optimize expr db)] + + (testing "eval" + (is (true? (expr/eval (eval-context db) expr nil)))) + + (testing "form" + (has-form expr + '(in-value-set + "code-115927" + (set (code "system-115910" "code-115927") + (code "system-115910" "code-105600")))))))) + + (testing "Concept" + (with-system-data [{:blaze.db/keys [node] terminology-service ::ts/local} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-105600"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (let [context + {:library + {:codeSystems + {:def [{:name "sys-def-115852" :id "system-115910" :version "version-132113"}]} + :valueSets + {:def + [{:name "value-set-def-135520" + :id "value-set-135750"}]}} + :terminology-service terminology-service} + elm #elm/in-value-set [#elm/concept [[#elm/code ["sys-def-115852" "code-115927"]]] + #elm/value-set-ref "value-set-def-135520"] + expr (c/compile context elm) + db (d/db node) + expr (c/optimize expr db)] + + (testing "eval" + (is (true? (expr/eval (eval-context db) expr nil)))) + + (testing "form" + (has-form expr + '(in-value-set + (concept (code "system-115910" "version-132113" "code-115927")) + (set (code "system-115910" "code-115927") + (code "system-115910" "code-105600"))))))))) ;; 23.9. Not Equal diff --git a/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj b/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj index ffd96bcee..c0349e613 100644 --- a/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj +++ b/modules/cql/test/blaze/elm/compiler/clinical_values_test.clj @@ -5,6 +5,9 @@ https://cql.hl7.org/04-logicalspecification.html." (:require [blaze.anomaly :as ba] + [blaze.async.comp :as ac] + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config]] [blaze.elm.code-spec] [blaze.elm.compiler :as c] [blaze.elm.compiler.clinical-values] @@ -17,10 +20,15 @@ [blaze.elm.literal-spec] [blaze.elm.quantity :refer [quantity]] [blaze.elm.ratio :refer [ratio]] - [blaze.test-util :refer [satisfies-prop]] + [blaze.elm.value-set :as value-set] + [blaze.module.test-util :refer [with-system]] + [blaze.terminology-service] + [blaze.terminology-service-spec] + [blaze.terminology-service.protocols :as p] + [blaze.test-util :refer [given-thrown satisfies-prop]] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [are deftest testing]] + [clojure.test :as test :refer [are deftest is testing]] [clojure.test.check.properties :as prop] [cognitect.anomalies :as anom] [juxt.iota :refer [given]]) @@ -386,3 +394,76 @@ (testing "form" (has-form expr '(ratio (quantity 3M "m") (quantity 1M "s")))))) + +;; 3.12. ValueSetRef +;; +;; The ValueSetRef expression allows a previously defined named value set to be +;; referenced within an expression. Conceptually, referencing a value set +;; returns the expansion set for the value set as a list of codes. +;; +;; The preserve attribute is trial-use in CQL 1.5.2 and was introduced to allow +;; engines to determine whether or not to expand a ValueSetRef (the 1.4 +;; behavior), ensuring that 1.5 engines can run 1.4 ELM. +(def ^:private not-found-terminology-service + (reify p/TerminologyService + (-expand-value-set [_ {:keys [db url]}] + (ac/completed-future + (ba/not-found + (format "The ValueSet with URL `%s` was not found." url) :t (d/t db)))))) + +(def ^:private expansion-terminology-service + (reify p/TerminologyService + (-expand-value-set [_ _] + (ac/completed-future + {:fhir/type :fhir/ValueSet + :expansion + {:fhir/type :fhir.ValueSet/expansion + :contains + [{:fhir/type :fhir.ValueSet.expansion/contains + :system #fhir/uri"system-102931" + :code #fhir/code"code-102402"} + {:fhir/type :fhir.ValueSet.expansion/contains + :system #fhir/uri"system-102938" + :code #fhir/code"code-102420"}]}})))) + +(deftest compile-value-set-ref-test + (testing "with terminology service returning not-found anomaly" + (let [context + {:library + {:valueSets + {:def + [{:name "value-set-def-135520" + :id "value-set-135750"}]}} + :terminology-service not-found-terminology-service} + expr (c/compile context #elm/value-set-ref "value-set-def-135520")] + + (testing "optimize" + (with-system [{:blaze.db/keys [node]} mem-node-config] + (given-thrown (c/optimize expr (d/db node)) + ::anom/category := ::anom/not-found + ::anom/message := "The ValueSet with URL `value-set-135750` was not found." + :t := 0))) + + (testing "form" + (has-form expr + '(value-set "value-set-135750"))))) + + (testing "with terminology service returning a value set with two expansions" + (let [context + {:library + {:valueSets + {:def + [{:name "value-set-def-135520" + :id "value-set-135750"}]}} + :terminology-service expansion-terminology-service} + expr (c/compile context #elm/value-set-ref "value-set-def-135520")] + + (testing "optimize" + (with-system [{:blaze.db/keys [node]} mem-node-config] + (is (= (c/optimize expr (d/db node)) + #{(value-set/->Code "system-102931" "code-102402") + (value-set/->Code "system-102938" "code-102420")})))) + + (testing "form" + (has-form expr + '(value-set "value-set-135750")))))) diff --git a/modules/cql/test/blaze/elm/compiler/external_data_test.clj b/modules/cql/test/blaze/elm/compiler/external_data_test.clj index 0b33b39d3..7db7c7021 100644 --- a/modules/cql/test/blaze/elm/compiler/external_data_test.clj +++ b/modules/cql/test/blaze/elm/compiler/external_data_test.clj @@ -12,6 +12,7 @@ [blaze.elm.compiler.core :as core] [blaze.elm.compiler.external-data] [blaze.elm.compiler.library :as library] + [blaze.elm.compiler.library-spec] [blaze.elm.compiler.test-util :as ctu :refer [has-form]] [blaze.elm.expression :as expr] [blaze.elm.expression-spec] @@ -20,6 +21,8 @@ [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type] [blaze.module.test-util :refer [with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] [cognitect.anomalies :as anom] @@ -37,6 +40,12 @@ (test/use-fixtures :each fixture) +(def config + (assoc mem-node-config ::ts/local {})) + +(defn- compile-context [{:blaze.db/keys [node] ::ts/keys [local]}] + {:node node :terminology-service local}) + (defn- eval-context [db] {:db db :now (time/offset-date-time)}) @@ -482,7 +491,7 @@ (testing "with related context" (testing "without code" - (with-system-data [{:blaze.db/keys [node]} mem-node-config] + (with-system-data [{:blaze.db/keys [node] :as system} config] [[[:put {:fhir/type :fhir/Patient :id "0"}] [:put {:fhir/type :fhir/Observation :id "0" :subject #fhir/Reference{:reference "Patient/0"}}]]] @@ -500,7 +509,8 @@ define InInitialPopulation: [\"name-133756\" -> Observation] ") - {:keys [expression-defs]} (library/compile-library node library {}) + context (compile-context system) + {:keys [expression-defs]} (library/compile-library context library {}) db (d/db node) patient (ctu/resource db "Patient" "0") eval-context (assoc (eval-context db) :expression-defs expression-defs) @@ -530,7 +540,7 @@ '(retrieve (singleton-from (retrieve-resource)) "Observation")))))) (testing "with pre-compiled database query" - (with-system-data [{:blaze.db/keys [node]} mem-node-config] + (with-system-data [{:blaze.db/keys [node] :as system} config] [[[:put {:fhir/type :fhir/Patient :id "0"}] [:put {:fhir/type :fhir/Observation :id "0" :code @@ -556,7 +566,8 @@ define InInitialPopulation: [\"name-133730\" -> Observation: Code 'code-133657' from sys] ") - {:keys [expression-defs]} (library/compile-library node library {}) + context (compile-context system) + {:keys [expression-defs]} (library/compile-library context library {}) db (d/db node) patient (ctu/resource db "Patient" "0") eval-context (assoc (eval-context db) :expression-defs expression-defs) diff --git a/modules/cql/test/blaze/elm/compiler/library_test.clj b/modules/cql/test/blaze/elm/compiler/library_test.clj index 246ccb804..f97828fc4 100644 --- a/modules/cql/test/blaze/elm/compiler/library_test.clj +++ b/modules/cql/test/blaze/elm/compiler/library_test.clj @@ -11,6 +11,8 @@ [blaze.fhir.spec.type :as type] [blaze.fhir.spec.type.system] [blaze.module.test-util :refer [with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] [blaze.test-util :as tu] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] @@ -24,6 +26,12 @@ (test/use-fixtures :each tu/fixture) +(def config + (assoc mem-node-config ::ts/local {})) + +(defn- compile-context [{:blaze.db/keys [node] ::ts/keys [local]}] + {:node node :terminology-service local}) + (def ^:private default-opts {}) (def ^:private expr-form (comp c/form :expression)) @@ -74,14 +82,14 @@ (deftest compile-library-test (testing "empty library" (let [library (t/translate "library Test")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) + (with-system [system config] + (given (library/compile-library (compile-context system) library default-opts) :expression-defs := {})))) (testing "one static expression" (let [library (t/translate "library Test define Foo: true")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["Foo" :context] := "Patient" ["Foo" :expression] := true) @@ -94,8 +102,8 @@ using FHIR version '4.0.0' context Patient define Gender: Patient.gender")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["Gender" :context] := "Patient" ["Gender" expr-form] := '(:gender (singleton-from (retrieve-resource)))) @@ -112,8 +120,8 @@ context Patient define function Gender(P Patient): P.gender define InInitialPopulation: Gender(Patient)")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs function-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs function-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" :resultTypeName] := "{http://hl7.org/fhir}AdministrativeGender" @@ -137,8 +145,8 @@ define function Inc(i System.Integer): i + 1 define function Inc2(i System.Integer): Inc(i) + 1 define InInitialPopulation: Inc2(1)")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := '(call "Inc2" 1)) @@ -175,8 +183,8 @@ define InInitialPopulation: Inclusion and not Exclusion")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -216,8 +224,8 @@ Patient.gender = 'female' and exists from [MedicationStatement] M where M.medication.reference in TemozolomidRefs")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["TemozolomidRefs" :context] := "Unfiltered" ["TemozolomidRefs" expr-form] := @@ -313,8 +321,8 @@ define Gender: Patient.gender")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["Patient" :context] := "Patient" ["Patient" expr-form] := '(singleton-from (retrieve-resource)) @@ -342,8 +350,8 @@ define InInitialPopulation: exists from [MedicationAdministration] M where M.medication.reference in {'Medication/0', 'Medication/1'}")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -386,8 +394,8 @@ define InInitialPopulation: exists from [MedicationAdministration] M where M.medication.reference in MedicationRefs")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -434,8 +442,8 @@ where M.medication.reference in MedicationRefsA) or exists (from [MedicationAdministration] M where M.medication.reference in MedicationRefsB)")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -487,8 +495,8 @@ where M.medication.reference in MedicationRefsA) or exists (from [MedicationAdministration] M where M.medication.reference in {'Medication/0'})")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -528,16 +536,16 @@ (testing "function" (let [library (t/translate "library Test define function Error(): singleton from {1, 2}")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) + (with-system [system config] + (given (library/compile-library (compile-context system) library default-opts) ::anom/category := ::anom/conflict ::anom/message := "More than one element in `SingletonFrom` expression.")))) (testing "expression" (let [library (t/translate "library Test define Error: singleton from {1, 2}")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) + (with-system [system config] + (given (library/compile-library (compile-context system) library default-opts) ::anom/category := ::anom/conflict ::anom/message := "More than one element in `SingletonFrom` expression."))))) @@ -552,8 +560,8 @@ define InInitialPopulation: Patient.gender = Gender")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts)] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts)] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -578,16 +586,16 @@ (testing "with parameter default" (let [library (t/translate "library Test parameter \"Measurement Period\" Interval default Interval[@2020-01-01, @2020-12-31]")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) + (with-system [system config] + (given (library/compile-library (compile-context system) library default-opts) [:parameter-default-values "Measurement Period" :start] := #system/date"2020-01-01" [:parameter-default-values "Measurement Period" :end] := #system/date"2020-12-31")))) (testing "with invalid parameter default" (let [library (t/translate "library Test parameter \"Measurement Start\" Integer default singleton from {1, 2}")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (given (library/compile-library node library default-opts) + (with-system [system config] + (given (library/compile-library (compile-context system) library default-opts) ::anom/category := ::anom/conflict ::anom/message "More than one element in `SingletonFrom` expression.")))) @@ -602,9 +610,9 @@ exists [Observation] O with [Encounter] E such that O.encounter.reference = 'Encounter/' + E.id")] - (with-system [{:blaze.db/keys [node]} mem-node-config] + (with-system [{:blaze.db/keys [node] :as system} config] (let [{:keys [expression-defs function-defs]} - (library/compile-library node library {})] + (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -647,8 +655,8 @@ define InInitialPopulation: [\"name-133756\" -> Observation]")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -678,8 +686,8 @@ define InInitialPopulation: exists [Condition: prostata]")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [system config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -705,8 +713,8 @@ exists [Condition] and exists [Encounter] and exists [Specimen]")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -742,8 +750,8 @@ define InInitialPopulation: Criterion_1 and Criterion_2")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts) + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts) in-initial-population (get expression-defs "InInitialPopulation")] (testing "after compilation the named expressions are resolved @@ -789,8 +797,8 @@ exists [Condition] or exists [Encounter] or exists [Specimen]")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -826,8 +834,8 @@ define InInitialPopulation: Criterion_1 or Criterion_2")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts) + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts) in-initial-population (get expression-defs "InInitialPopulation")] (testing "after compilation the named expressions are resolved @@ -879,8 +887,8 @@ define InInitialPopulation: Criterion_1 and Criterion_2")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library default-opts) + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library default-opts) in-initial-population (get expression-defs "InInitialPopulation")] (testing "after compilation the named expressions are resolved @@ -927,8 +935,8 @@ define InInitialPopulation: Patient.identifier.where(type ~ Code 'GKV' from IdentifierType).exists()")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -967,11 +975,11 @@ define InInitialPopulation: exists [Observation: Code '788-0' from loinc]")] - (with-system-data [{:blaze.db/keys [node]} mem-node-config] + (with-system-data [{:blaze.db/keys [node] :as system} config] [[[:put {:fhir/type :fhir/Observation :id "0" :code (codeable-concept "http://loinc.org" "788-0")}]]] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -995,8 +1003,8 @@ define InInitialPopulation: exists [Observation: Code '788-0' from loinc]")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -1023,11 +1031,11 @@ define InInitialPopulation: exists [Observation: body_weight] O where O.value < 3.3 'kg'")] - (with-system-data [{:blaze.db/keys [node]} mem-node-config] + (with-system-data [{:blaze.db/keys [node] :as system} config] [[[:put {:fhir/type :fhir/Observation :id "0" :code (codeable-concept "http://loinc.org" "29463-7")}]]] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -1057,8 +1065,8 @@ define InInitialPopulation: exists [Observation: body_weight] O where O.value < 3.3 'kg'")] - (with-system [{:blaze.db/keys [node]} mem-node-config] - (let [{:keys [expression-defs]} (library/compile-library node library {})] + (with-system [{:blaze.db/keys [node] :as system} config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] (given expression-defs ["InInitialPopulation" :context] := "Patient" ["InInitialPopulation" expr-form] := @@ -1076,4 +1084,25 @@ (testing "the whole expression will be optimized to false" (given (library/optimize (d/db node) expression-defs) ["InInitialPopulation" :context] := "Patient" - ["InInitialPopulation" expr-form] := false)))))))) + ["InInitialPopulation" expr-form] := false))))))) + + (testing "valueset" + (let [library (t/translate "library test + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + valueset FemaleAdministrativeSex: + 'urn:oid:2.16.840.1.113883.3.560.100.2' + + context Patient + + define InInitialPopulation: + Patient.gender in FemaleAdministrativeSex")] + (with-system [system config] + (let [{:keys [expression-defs]} (library/compile-library (compile-context system) library {})] + (given expression-defs + ["InInitialPopulation" :context] := "Patient" + ["InInitialPopulation" expr-form] := + '(in-value-set + (call "ToString" (:gender (singleton-from (retrieve-resource)))) + (value-set "urn:oid:2.16.840.1.113883.3.560.100.2")))))))) diff --git a/modules/cql/test/blaze/elm/literal.clj b/modules/cql/test/blaze/elm/literal.clj index d9fe597ad..22f4055d7 100644 --- a/modules/cql/test/blaze/elm/literal.clj +++ b/modules/cql/test/blaze/elm/literal.clj @@ -115,6 +115,10 @@ denominator-unit (assoc :unit denominator-unit))}) +;; 3.12. ValueSetRef +(defn value-set-ref [name] + {:type "ValueSetRef" :name name}) + ;; 7. Parameters ;; 7.2. ParameterRef @@ -905,3 +909,9 @@ :operand [x y]} precision (assoc :precision precision))) + +;; 23.8. InValueSet +(defn in-value-set [[code value-set]] + {:type "InValueSet" + :code code + :valueset value-set}) diff --git a/modules/cql/test/data_readers.clj b/modules/cql/test/data_readers.clj index cac8a87ea..20f69b317 100644 --- a/modules/cql/test/data_readers.clj +++ b/modules/cql/test/data_readers.clj @@ -13,6 +13,7 @@ elm/concept-ref blaze.elm.literal/concept-ref elm/quantity blaze.elm.literal/quantity elm/ratio blaze.elm.literal/ratio + elm/value-set-ref blaze.elm.literal/value-set-ref elm/parameter-ref blaze.elm.literal/parameter-ref elm/expression-ref blaze.elm.literal/expression-ref elm/function-ref blaze.elm.literal/function-ref @@ -128,5 +129,6 @@ elm/to-ratio blaze.elm.literal/to-ratio elm/to-string blaze.elm.literal/to-string elm/calculate-age-at blaze.elm.literal/calculate-age-at + elm/in-value-set blaze.elm.literal/in-value-set ctu/optimizeable blaze.elm.compiler.test-util/optimizeable ctu/optimize-to blaze.elm.compiler.test-util/optimize-to} diff --git a/modules/extern-terminology-service/src/blaze/terminology_service/extern.clj b/modules/extern-terminology-service/src/blaze/terminology_service/extern.clj index ea0dc276a..13e423319 100644 --- a/modules/extern-terminology-service/src/blaze/terminology_service/extern.clj +++ b/modules/extern-terminology-service/src/blaze/terminology_service/extern.clj @@ -6,13 +6,14 @@ [blaze.module :as m :refer [reg-collector]] [blaze.terminology-service :as ts] [blaze.terminology-service.extern.spec] + [blaze.terminology-service.protocols :as p] [clojure.spec.alpha :as s] [integrant.core :as ig] [prometheus.alpha :as prom :refer [defhistogram]] [taoensso.timbre :as log]) (:import - [com.github.benmanes.caffeine.cache Caffeine AsyncCacheLoader - AsyncLoadingCache])) + [com.github.benmanes.caffeine.cache + AsyncCacheLoader AsyncLoadingCache Caffeine])) (set! *warn-on-reflection* true) @@ -21,40 +22,37 @@ {:namespace "terminology_service"} (take 14 (iterate #(* 2 %) 0.001))) -(defn- expand-value-set* [base-uri http-client params] - (log/debug "Expand ValueSet with params" (pr-str params)) +(defn- expand-value-set* [base-uri http-client url] + (log/debug "Expand ValueSet with url" url) (fhir-client/execute-type-get base-uri "ValueSet" "expand" {:http-client http-client - :query-params params})) + :query-params {:url url}})) -(defn- expand-value-set [base-uri http-client params] +(defn- expand-value-set [base-uri http-client url] (let [timer (prom/timer request-duration-seconds)] - (-> (expand-value-set* base-uri http-client params) + (-> (expand-value-set* base-uri http-client url) (ac/when-complete (fn [_ _] (prom/observe-duration! timer)))))) (defn- cache [base-uri http-client] (-> (Caffeine/newBuilder) (.maximumSize 1000) - (.buildAsync - (reify AsyncCacheLoader - (asyncLoad [_ params _] - (expand-value-set base-uri http-client params)))))) + (^[AsyncCacheLoader] Caffeine/.buildAsync + (fn [request _] + (expand-value-set base-uri http-client (:url request)))))) -(defrecord TerminologyService [^AsyncLoadingCache cache] - ts/TerminologyService - (-expand-value-set [_ params] - (.get cache params))) - -(defmethod m/pre-init-spec :blaze.terminology-service/extern [_] +(defmethod m/pre-init-spec ::ts/extern [_] (s/keys :req-un [::base-uri :blaze/http-client])) -(defmethod ig/init-key :blaze.terminology-service/extern +(defmethod ig/init-key ::ts/extern [_ {:keys [base-uri http-client]}] (log/info (str "Init terminology server connection: " base-uri)) - (->TerminologyService (cache base-uri http-client))) + (let [cache (cache base-uri http-client)] + (reify p/TerminologyService + (-expand-value-set [_ request] + (.get ^AsyncLoadingCache cache request))))) -(derive :blaze.terminology-service/extern :blaze/terminology-service) +(derive ::ts/extern :blaze/terminology-service) (reg-collector ::request-duration-seconds request-duration-seconds) diff --git a/modules/extern-terminology-service/test/blaze/terminology_service/extern_test.clj b/modules/extern-terminology-service/test/blaze/terminology_service/extern_test.clj index d8cb2bf94..429d5d117 100644 --- a/modules/extern-terminology-service/test/blaze/terminology_service/extern_test.clj +++ b/modules/extern-terminology-service/test/blaze/terminology_service/extern_test.clj @@ -4,6 +4,7 @@ [blaze.http-client.spec :refer [http-client?]] [blaze.module.test-util :refer [with-system]] [blaze.terminology-service :as ts] + [blaze.terminology-service-spec] [blaze.terminology-service.extern] [blaze.test-util :as tu :refer [given-thrown]] [clojure.spec.alpha :as s] diff --git a/modules/fhir-structure/src/blaze/fhir/spec/spec.clj b/modules/fhir-structure/src/blaze/fhir/spec/spec.clj index d5d4958ca..07ff2a110 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/spec.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/spec.clj @@ -54,3 +54,6 @@ (s/def :fhir/dateTime #(s2/valid? :fhir/dateTime %)) + +(s/def :fhir/ValueSet + #(s2/valid? :fhir/ValueSet %)) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj index 661376238..0be8f82e1 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure.clj @@ -19,6 +19,7 @@ [blaze.luid :as luid] [blaze.module :as m :refer [reg-collector]] [blaze.spec] + [blaze.terminology-service.spec] [blaze.util :as u] [clojure.spec.alpha :as s] [cognitect.anomalies :as anom] @@ -125,7 +126,8 @@ (ac/then-compose-async (partial handle (assoc context :db db) request))))) (defmethod m/pre-init-spec ::handler [_] - (s/keys :req-un [:blaze.db/node ::executor :blaze/clock :blaze/rng-fn] + (s/keys :req-un [:blaze.db/node :blaze/terminology-service ::executor + :blaze/clock :blaze/rng-fn] :opt [::expr/cache] :opt-un [::timeout :blaze/context-path])) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj index 81d600fb9..f27cca1c2 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure.clj @@ -80,22 +80,22 @@ using `node`. Returns an anomaly on errors." - [node library opts] + [context library opts] (when-ok [cql-code (extract-cql-code library) library (translate cql-code)] - (library/compile-library node library opts))) + (library/compile-library context library opts))) (defn- compile-library "Compiles the CQL code from the first attachment in the `library` resource using `node`. Returns an anomaly on errors." - {:arglists '([node library opts])} - [node {:keys [id] :as library} opts] + {:arglists '([context library opts])} + [context {:keys [id] :as library} opts] (log/debug (format "Start compiling Library with ID `%s`..." id)) (let [timer (prom/timer compile-duration-seconds)] (try - (compile-library* node library opts) + (compile-library* context library opts) (finally (let [duration (prom/observe-duration! timer)] (log/debug @@ -144,10 +144,11 @@ compilation. Returns an anomaly on errors." - [db measure opts] + [db terminology-service measure opts] (if-let [library-ref (-> measure :library first type/value)] (do-sync [library (find-library db library-ref)] - (compile-library (d/node db) library opts)) + (let [context {:node (d/node db) :terminology-service terminology-service}] + (compile-library context library opts))) (ac/completed-future (ba/unsupported "Missing primary library. Currently only CQL expressions together with one primary library are supported." @@ -425,7 +426,7 @@ :timeout timeout)))) (defn- enhance-context - [{:keys [clock db timeout] + [{:keys [clock db timeout terminology-service] :blaze/keys [cancelled?] ::expr/keys [cache] :or {timeout (time/hours 1)} @@ -435,7 +436,7 @@ now (time/offset-date-time clock) timeout-eclipsed? (timeout-eclipsed-fn clock now timeout)] (do-sync [{:keys [expression-defs function-defs parameter-default-values]} - (compile-primary-library db measure {})] + (compile-primary-library db terminology-service measure {})] (when-ok [subject-handle (some->> subject-ref (subject-handle db subject-type))] (let [optimize (partial library/optimize db) context diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj index 311d7bca8..6bee43918 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_test.clj @@ -6,6 +6,7 @@ [blaze.db.api :as d] [blaze.db.api-stub :refer [mem-node-config with-system-data]] [blaze.elm.compiler.library :as library] + [blaze.elm.compiler.library-spec] [blaze.elm.expression :as expr] [blaze.fhir.operation.evaluate-measure.cql :as cql] [blaze.fhir.operation.evaluate-measure.cql-spec] @@ -13,6 +14,8 @@ [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type] [blaze.module.test-util :refer [given-failed-future with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] [blaze.test-util :as tu] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] @@ -78,9 +81,9 @@ define InInitialPopulation: true") -(defn- compile-library [node cql] +(defn- compile-library [{:blaze.db/keys [node] ::ts/keys [local]} cql] (when-ok [library (cql-translator/translate cql)] - (library/compile-library node library {}))) + (library/compile-library {:node node :terminology-service local} library {}))) (defn- failing-eval [msg] (fn [_ _ _] (throw (Exception. ^String msg)))) @@ -88,9 +91,10 @@ (defn- context [{:blaze.db/keys [node] ::expr/keys [cache] - :blaze.test/keys [fixed-clock executor]} + :blaze.test/keys [fixed-clock executor] + :as system} library] - (let [{:keys [expression-defs function-defs]} (compile-library node library)] + (let [{:keys [expression-defs function-defs]} (compile-library system library)] {:db (d/db node) :now (time/offset-date-time fixed-clock) ::expr/cache cache @@ -104,6 +108,7 @@ ::expr/cache {:node (ig/ref :blaze.db/node) :executor (ig/ref :blaze.test/executor)} + ::ts/local {} :blaze.test/executor {})) (def ^:private conj-reduce-op diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_test.clj index d4c807f7b..b6a8f4e64 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_test.clj @@ -6,10 +6,13 @@ [blaze.db.api :as d] [blaze.db.api-stub :refer [mem-node-config with-system-data]] [blaze.elm.compiler.library :as library] + [blaze.elm.compiler.library-spec] [blaze.fhir.operation.evaluate-measure.measure.stratifier :as strat] [blaze.fhir.operation.evaluate-measure.measure.stratifier-spec] [blaze.fhir.operation.evaluate-measure.test-util :as em-tu] [blaze.module.test-util :refer [with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] [blaze.test-util :as tu] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] @@ -22,9 +25,9 @@ (test/use-fixtures :each tu/fixture) -(defn- compile-library [node cql] +(defn- compile-library [{:blaze.db/keys [node] ::ts/keys [local]} cql] (when-ok [library (cql-translator/translate cql)] - (library/compile-library node library {}))) + (library/compile-library {:node node :terminology-service local} library {}))) (def empty-library "library Retrieve @@ -152,8 +155,9 @@ :criteria (cql-expression "Age")}]}) (defn- context - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock executor]} library] - (let [{:keys [expression-defs function-defs]} (compile-library node library)] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock executor] :as system} + library] + (let [{:keys [expression-defs function-defs]} (compile-library system library)] (cond-> {:db (d/db node) :now (time/offset-date-time fixed-clock) @@ -165,6 +169,7 @@ (def ^:private config (assoc mem-node-config + ::ts/local {} :blaze.test/executor {})) (deftest reduce-op-test diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj index 86ca66de6..5c01b40c6 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_spec.clj @@ -12,6 +12,7 @@ [blaze.fhir.spec] [blaze.http.spec] [blaze.spec] + [blaze.terminology-service.spec] [clojure.spec.alpha :as s] [reitit.core :as reitit]) (:import @@ -21,7 +22,7 @@ (s/keys :req [:blaze/base-url ::reitit/router] :opt [:blaze/cancelled? ::expr/cache] :req-un [:blaze/clock :blaze/rng-fn :blaze.db/db - ::evaluate-measure/executor] + ::evaluate-measure/executor :blaze/terminology-service] :opt-un [::evaluate-measure/timeout])) (defn- temporal? [x] diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj index f17f59722..509709cf8 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure_test.clj @@ -15,6 +15,8 @@ [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type :as type] [blaze.module.test-util :refer [given-failed-future]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] [blaze.test-util :as tu] [clojure.java.io :as io] [clojure.spec.alpha :as s] @@ -81,6 +83,7 @@ ::expr/cache {:node (ig/ref :blaze.db/node) :executor (ig/ref :blaze.test/executor)} + ::ts/local {} :blaze.test/fixed-rng-fn {} :blaze.test/executor {})) @@ -93,13 +96,14 @@ (with-system-data [{:blaze.db/keys [node] ::expr/keys [cache] - :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [(tx-ops (:entry (read-data name)))] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db ::expr/cache cache :blaze/base-url "" ::reitit/router router - :executor executor} + :terminology-service local :executor executor} measure @(d/pull node (d/resource-handle db "Measure" "0")) period [#system/date "2000" #system/date "2020"] params {:period period :report-type report-type} @@ -234,7 +238,8 @@ (deftest evaluate-measure-test (testing "Encounter population basis" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0"}] [:put {:fhir/type :fhir/Encounter :id "0-0" :subject #fhir/Reference{:reference "Patient/0"}}] [:put {:fhir/type :fhir/Patient :id "1"}] @@ -246,7 +251,8 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - :executor executor :blaze/base-url "" ::reitit/router router} + :executor executor :terminology-service local + :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -288,7 +294,8 @@ (testing "two groups" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}] [:put {:fhir/type :fhir/Patient :id "1" :gender #fhir/code"female"}] [:put {:fhir/type :fhir/Encounter :id "1-0" :subject #fhir/Reference{:reference "Patient/1"}}] @@ -304,7 +311,8 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - :executor executor :blaze/base-url "" ::reitit/router router} + :executor executor :terminology-service local + :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -404,7 +412,8 @@ (testing "library with syntax error" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content "library Test define Error: (")]}]]] @@ -412,7 +421,7 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/base-url "" ::reitit/router router - :executor executor} + :executor executor :terminology-service local} measure-id "measure-id-133021" measure {:fhir/type :fhir/Measure :id measure-id :library [#fhir/canonical"0"] @@ -433,14 +442,15 @@ (testing "missing criteria" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content (library-gender true))]}]]] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/base-url "" ::reitit/router router - :executor executor} + :executor executor :terminology-service local} measure-id "measure-id-133021" measure {:fhir/type :fhir/Measure :id measure-id :library [#fhir/canonical"0"] @@ -461,7 +471,8 @@ (testing "evaluation timeout" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0"}]] [[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content (library-gender true))]}]]] @@ -469,7 +480,7 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :timeout (time/seconds 0) - :executor executor + :executor executor :terminology-service local :blaze/base-url "" ::reitit/router router} measure-id "measure-id-132321" measure {:fhir/type :fhir/Measure :id measure-id @@ -492,7 +503,8 @@ (testing "cancellation" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0"}]] [[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content (library-gender true))]}]]] @@ -501,7 +513,7 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/cancelled? (constantly cancelled?) - :executor executor + :executor executor :terminology-service local :blaze/base-url "" ::reitit/router router} measure-id "measure-id-132321" measure {:fhir/type :fhir/Measure :id measure-id @@ -528,7 +540,8 @@ (testing "Encounter population basis" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0"}] [:put {:fhir/type :fhir/Encounter :id "0-0" :subject #fhir/Reference{:reference "Patient/0"}}] [:put {:fhir/type :fhir/Patient :id "1"}] @@ -542,7 +555,7 @@ context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/cancelled? (constantly (ba/interrupted "msg-114556")) - :executor executor + :executor executor :terminology-service local :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] @@ -570,14 +583,15 @@ (doseq [subject-ref ["0" ["Patient" "0"]] [library count] [[(library true) 1] [(library false) 0]]] (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0"}] [:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content library)]}]]] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - :executor executor + :executor executor :terminology-service local :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :url #fhir/uri"measure-155437" @@ -607,14 +621,15 @@ (testing "with stratifiers" (doseq [[library count] [[(library-gender true) 1] [(library-gender false) 0]]] (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}] [:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content library)]}]]] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - :executor executor + :executor executor :terminology-service local :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :url #fhir/uri"measure-155502" @@ -650,14 +665,15 @@ (testing "invalid subject" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content (library-gender true))]}]]] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/base-url "" ::reitit/router router - :executor executor} + :executor executor :terminology-service local} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -676,14 +692,15 @@ (testing "missing subject" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content (library-gender true))]}]]] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/base-url "" ::reitit/router router - :executor executor} + :executor executor :terminology-service local} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -702,7 +719,8 @@ (testing "deleted subject" (with-system-data - [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Patient :id "0"}] [:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content (library-gender true))]}]] @@ -711,7 +729,7 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db :blaze/base-url "" ::reitit/router router - :executor executor} + :executor executor :terminology-service local} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -753,15 +771,17 @@ (with-system-data [{:blaze.db/keys [node] ::expr/keys [cache] - :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [(into [] (mapcat patient-condition-tx-ops) (range 2000)) [[:put {:fhir/type :fhir/Library :id "0" :url #fhir/uri"0" :content [(library-content library-exists-condition)]}]]] (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - ::expr/cache cache - :executor executor :blaze/base-url "" ::reitit/router router} + ::expr/cache cache :executor executor + :terminology-service local + :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -800,7 +820,8 @@ (with-system-data [{:blaze.db/keys [node] ::expr/keys [cache] - :blaze.test/keys [fixed-clock fixed-rng-fn executor]} config] + :blaze.test/keys [fixed-clock fixed-rng-fn executor] + ::ts/keys [local]} config] [[[:put {:fhir/type :fhir/Medication :id "0" :code #fhir/CodeableConcept{:coding [#fhir/Coding{:system #fhir/uri"http://fhir.de/CodeSystem/dimdi/atc" :code #fhir/code"L01AX03"}]}}]] (into [] (mapcat patient-medication-tx-ops) (range 2000)) @@ -809,8 +830,9 @@ (let [db (d/db node) context {:clock fixed-clock :rng-fn fixed-rng-fn :db db - ::expr/cache cache - :executor executor :blaze/base-url "" ::reitit/router router} + ::expr/cache cache :executor executor + :terminology-service local + :blaze/base-url "" ::reitit/router router} measure {:fhir/type :fhir/Measure :id "0" :library [#fhir/canonical"0"] :group @@ -937,6 +959,8 @@ (testing-query "q60-medication" 2) + (testing-query "q61-in-value-set-gender" 1) + (let [result (evaluate "q1" "subject-list")] (testing "MeasureReport is valid" (is (s/valid? :blaze/resource (:resource result)))) @@ -1199,4 +1223,4 @@ (comment (log/set-min-level! :debug) - (evaluate "q60-medication")) + (evaluate "q61-in-value-set-gender")) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.cql new file mode 100644 index 000000000..95d44bf98 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.cql @@ -0,0 +1,10 @@ +library "q61-in-value-set-gender" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +valueset FemaleAdministrativeSex: 'urn:oid:2.16.840.1.113883.3.560.100.2' + +context Patient + +define InInitialPopulation: + Patient.gender in FemaleAdministrativeSex diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.json b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.json new file mode 100644 index 000000000..5996578f9 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q61-in-value-set-gender.json @@ -0,0 +1,133 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "0", + "gender": "male" + }, + "request": { + "method": "PUT", + "url": "Patient/0" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "1", + "gender": "female" + }, + "request": { + "method": "PUT", + "url": "Patient/1" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "2" + }, + "request": { + "method": "PUT", + "url": "Patient/2" + } + }, + { + "resource": { + "resourceType": "ValueSet", + "id": "0", + "url": "urn:oid:2.16.840.1.113883.3.560.100.2", + "compose": { + "include": [ + { + "system": "AdministrativeGender", + "concept": [ + { + "code": "female" + } + ] + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/0" + } + }, + { + "resource": { + "resourceType": "CodeSystem", + "id": "0", + "url": "AdministrativeGender", + "concept": [ + { + "code" : "female", + "display" : "Female" + }, + { + "code" : "male", + "display" : "Male" + } + ] + }, + "request": { + "method": "PUT", + "url": "CodeSystem/0" + } + }, + { + "resource": { + "resourceType": "Measure", + "id": "0", + "url": "0", + "status": "active", + "subjectCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/resource-types", + "code": "Patient" + } + ] + }, + "library": [ + "0" + ], + "scoring": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "InInitialPopulation" + } + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Measure/0" + } + } + ] +} diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj index 8a76a9c97..614b4164d 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj @@ -8,6 +8,7 @@ [blaze.elm.expression :as-alias expr] [blaze.executors :as ex] [blaze.fhir.operation.evaluate-measure :as evaluate-measure] + [blaze.fhir.operation.evaluate-measure.measure-spec] [blaze.fhir.operation.evaluate-measure.test-util :refer [wrap-error]] [blaze.fhir.spec.type :as type] [blaze.fhir.test-util] @@ -15,6 +16,9 @@ [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.module.test-util :refer [with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.local] + [blaze.terminology-service.spec :refer [terminology-service?]] [blaze.test-util :as tu :refer [given-thrown]] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] @@ -71,6 +75,7 @@ ::evaluate-measure/handler {:node (ig/ref :blaze.db/node) ::expr/cache (ig/ref ::expr/cache) + :terminology-service (ig/ref :blaze/terminology-service) :executor (ig/ref :blaze.test/executor) :clock (ig/ref :blaze.test/fixed-clock) :rng-fn (ig/ref :blaze.test/fixed-rng-fn)} @@ -81,6 +86,7 @@ ::expr/cache {:node (ig/ref :blaze.db/node) :executor (ig/ref :blaze.test/executor)} + ::ts/local {} :blaze.test/executor {} :blaze.test/fixed-rng-fn {})) @@ -96,29 +102,43 @@ :key := ::evaluate-measure/handler :reason := ::ig/build-failed-spec [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) - [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)) - [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :clock)) - [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)))) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :terminology-service)) + [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :executor)) + [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :clock)) + [:cause-data ::s/problems 4 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)))) (testing "invalid node" (given-thrown (ig/init {::evaluate-measure/handler {:node ::invalid}}) :key := ::evaluate-measure/handler :reason := ::ig/build-failed-spec - [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :executor)) - [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :clock)) - [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)) - [:cause-data ::s/problems 3 :pred] := `node? - [:cause-data ::s/problems 3 :val] := ::invalid)) + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :terminology-service)) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)) + [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :clock)) + [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)) + [:cause-data ::s/problems 4 :pred] := `node? + [:cause-data ::s/problems 4 :val] := ::invalid)) + + (testing "invalid terminology-service" + (given-thrown (ig/init {::evaluate-measure/handler {:terminology-service ::invalid}}) + :key := ::evaluate-measure/handler + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)) + [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :clock)) + [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)) + [:cause-data ::s/problems 4 :pred] := `terminology-service? + [:cause-data ::s/problems 4 :val] := ::invalid)) (testing "invalid executor" (given-thrown (ig/init {::evaluate-measure/handler {:executor ::invalid}}) :key := ::evaluate-measure/handler :reason := ::ig/build-failed-spec [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) - [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :clock)) - [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)) - [:cause-data ::s/problems 3 :pred] := `ex/executor? - [:cause-data ::s/problems 3 :val] := ::invalid)) + [:cause-data ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :terminology-service)) + [:cause-data ::s/problems 2 :pred] := `(fn ~'[%] (contains? ~'% :clock)) + [:cause-data ::s/problems 3 :pred] := `(fn ~'[%] (contains? ~'% :rng-fn)) + [:cause-data ::s/problems 4 :pred] := `ex/executor? + [:cause-data ::s/problems 4 :val] := ::invalid)) (testing "init" (with-system [{::evaluate-measure/keys [handler]} config] @@ -378,7 +398,7 @@ (doseq [library-ref [#fhir/canonical"library-url-094115" #fhir/canonical"Library/0" #fhir/canonical"/Library/0"] - cancelled? [nil (constantly nil)]] + cancelled? [nil (constantly nil)]] (with-handler [handler] [[[:put {:fhir/type :fhir/Measure :id "0" diff --git a/modules/operation-value-set-expand/.clj-kondo/config.edn b/modules/operation-value-set-expand/.clj-kondo/config.edn new file mode 100644 index 000000000..c76c2d709 --- /dev/null +++ b/modules/operation-value-set-expand/.clj-kondo/config.edn @@ -0,0 +1,9 @@ +{:config-paths + ["../../../.clj-kondo/root" + "../../anomaly/resources/clj-kondo.exports/blaze/anomaly" + "../../async/resources/clj-kondo.exports/blaze/async" + "../../db-stub/resources/clj-kondo.exports/blaze/db-stub" + "../../module-test-util/resources/clj-kondo.exports/blaze/module-test-util"] + + :lint-as + {blaze.fhir.operation.value-set.expand-test/with-handler clojure.core/fn}} diff --git a/modules/operation-value-set-expand/Makefile b/modules/operation-value-set-expand/Makefile new file mode 100644 index 000000000..6ddac5bcb --- /dev/null +++ b/modules/operation-value-set-expand/Makefile @@ -0,0 +1,25 @@ +fmt: + cljfmt check src test deps.edn tests.edn + +lint: + clj-kondo --lint src test deps.edn + +prep: + clojure -X:deps prep + +test: prep + clojure -M:test:kaocha --profile :ci + +test-coverage: prep + clojure -M:test:coverage + +cloc-prod: + cloc src + +cloc-test: + cloc test + +clean: + rm -rf .clj-kondo/.cache .cpcache target + +.PHONY: fmt lint prep test test-coverage cloc-prod cloc-test clean diff --git a/modules/operation-value-set-expand/README.md b/modules/operation-value-set-expand/README.md new file mode 100644 index 000000000..0b5876929 --- /dev/null +++ b/modules/operation-value-set-expand/README.md @@ -0,0 +1,15 @@ +# Module - Operation \$expand on ValueSet + +> [!CAUTION] +> The operation \$expand on ValueSet is currently **beta**. Only a basic functionality is implemented. + +The \$expand operation can be used to expand all codes of a ValueSet. + +``` +GET [base]/ValueSet/$expand +GET [base]/ValueSet/[id]/$expand +``` + +The official documentation can be found [here][1]. + +[1]: diff --git a/modules/operation-value-set-expand/deps.edn b/modules/operation-value-set-expand/deps.edn new file mode 100644 index 000000000..8c22ed6d6 --- /dev/null +++ b/modules/operation-value-set-expand/deps.edn @@ -0,0 +1,31 @@ +{:deps + {blaze/module-base + {:local/root "../module-base"} + + blaze/rest-util + {:local/root "../rest-util"} + + blaze/terminology-service + {:local/root "../terminology-service"}} + + :aliases + {:test + {:extra-paths ["test"] + + :extra-deps + {blaze/db-stub + {:local/root "../db-stub"}}} + + :kaocha + {:extra-deps + {lambdaisland/kaocha + {:mvn/version "1.91.1392"}} + + :main-opts ["-m" "kaocha.runner"]} + + :coverage + {:extra-deps + {lambdaisland/kaocha-cloverage + {:mvn/version "1.1.89"}} + + :main-opts ["-m" "kaocha.runner" "--profile" "coverage"]}}} diff --git a/modules/operation-value-set-expand/src/blaze/fhir/operation/value_set/expand.clj b/modules/operation-value-set-expand/src/blaze/fhir/operation/value_set/expand.clj new file mode 100644 index 000000000..f7fd21169 --- /dev/null +++ b/modules/operation-value-set-expand/src/blaze/fhir/operation/value_set/expand.clj @@ -0,0 +1,32 @@ +(ns blaze.fhir.operation.value-set.expand + "Main entry point into the ValueSet $expand operation." + (:require + [blaze.anomaly :as ba] + [blaze.async.comp :as ac :refer [do-sync]] + [blaze.module :as m] + [blaze.terminology-service :as ts] + [blaze.terminology-service.spec] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [ring.util.response :as ring] + [taoensso.timbre :as log])) + +(defn- expand-value-set + [terminology-service {:blaze/keys [db] {:keys [id]} :path-params {:strs [url]} :query-params}] + (cond + id (ts/expand-value-set terminology-service {:db db :id id}) + url (ts/expand-value-set terminology-service {:db db :url url}) + :else (ac/completed-future (ba/incorrect "Missing required parameter `url`.")))) + +(defn- handler [terminology-service] + (fn [request] + (do-sync [value-set (expand-value-set terminology-service request)] + (ring/response value-set)))) + +(defmethod m/pre-init-spec :blaze.fhir.operation.value-set/expand [_] + (s/keys :req-un [:blaze/terminology-service])) + +(defmethod ig/init-key :blaze.fhir.operation.value-set/expand + [_ {:keys [terminology-service]}] + (log/info "Init FHIR ValueSet $expand operation handler") + (handler terminology-service)) diff --git a/modules/operation-value-set-expand/test/blaze/fhir/operation/value_set/expand_test.clj b/modules/operation-value-set-expand/test/blaze/fhir/operation/value_set/expand_test.clj new file mode 100644 index 000000000..6ceef922e --- /dev/null +++ b/modules/operation-value-set-expand/test/blaze/fhir/operation/value_set/expand_test.clj @@ -0,0 +1,153 @@ +(ns blaze.fhir.operation.value-set.expand-test + (:require + [blaze.async.comp :as ac] + [blaze.db.api-stub :as api-stub :refer [with-system-data]] + [blaze.fhir.operation.value-set.expand] + [blaze.handler.util :as handler-util] + [blaze.middleware.fhir.db :as db] + [blaze.terminology-service.local] + [blaze.terminology-service.spec :refer [terminology-service?]] + [blaze.test-util :as tu :refer [given-thrown]] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]] + [integrant.core :as ig] + [juxt.iota :refer [given]] + [taoensso.timbre :as log])) + +(st/instrument) +(log/set-min-level! :trace) + +(test/use-fixtures :each tu/fixture) + +(deftest init-test + (testing "nil config" + (given-thrown (ig/init {:blaze.fhir.operation.value-set/expand nil}) + :key := :blaze.fhir.operation.value-set/expand + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `map?)) + + (testing "missing config" + (given-thrown (ig/init {:blaze.fhir.operation.value-set/expand {}}) + :key := :blaze.fhir.operation.value-set/expand + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :terminology-service)))) + + (testing "invalid structure definition repo" + (given-thrown (ig/init {:blaze.fhir.operation.value-set/expand {:terminology-service ::invalid}}) + :key := :blaze.fhir.operation.value-set/expand + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `terminology-service? + [:cause-data ::s/problems 0 :val] := ::invalid))) + +(def config + (assoc api-stub/mem-node-config + :blaze.fhir.operation.value-set/expand + {:terminology-service (ig/ref :blaze.terminology-service/local)} + :blaze.terminology-service/local {})) + +(defn wrap-error [handler] + (fn [request] + (-> (handler request) + (ac/exceptionally handler-util/error-response)))) + +(defmacro with-handler [[handler-binding & [node-binding]] & more] + (let [[txs body] (api-stub/extract-txs-body more)] + `(with-system-data [{node# :blaze.db/node + handler# :blaze.fhir.operation.value-set/expand} config] + ~txs + (let [~handler-binding (-> handler# (db/wrap-db node# 100) wrap-error) + ~(or node-binding '_) node#] + ~@body)))) + +(deftest handler-test + (testing "ValueSet not found" + (testing "by id" + (with-handler [handler] + (let [{:keys [status body]} + @(handler {:path-params {:id "170852"}})] + + (is (= 404 status)) + + (given body + :fhir/type := :fhir/OperationOutcome + [:issue 0 :severity] := #fhir/code"error" + [:issue 0 :code] := #fhir/code"not-found" + [:issue 0 :diagnostics] := "The ValueSet with id `170852` was not found.")))) + + (testing "by url" + (with-handler [handler] + (let [{:keys [status body]} + @(handler {:query-params {"url" "153404"}})] + + (is (= 404 status)) + + (given body + :fhir/type := :fhir/OperationOutcome + [:issue 0 :severity] := #fhir/code"error" + [:issue 0 :code] := #fhir/code"not-found" + [:issue 0 :diagnostics] := "The ValueSet with URL `153404` was not found.")))) + + (testing "without url or id" + (with-handler [handler] + (let [{:keys [status body]} + @(handler {})] + + (is (= 400 status)) + + (given body + :fhir/type := :fhir/OperationOutcome + [:issue 0 :severity] := #fhir/code"error" + [:issue 0 :code] := #fhir/code"invalid" + [:issue 0 :diagnostics] := "Missing required parameter `url`."))))) + + (testing "successful expansion by id" + (with-handler [handler] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"}]}] + [:put {:fhir/type :fhir/ValueSet :id "152952" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (let [{:keys [status body]} + @(handler {:path-params {:id "152952"}})] + + (is (= 200 status)) + + (given body + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927")))) + + (testing "successful expansion by url" + (with-handler [handler] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-154043" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (let [{:keys [status body]} + @(handler {:query-params {"url" "value-set-154043"}})] + + (is (= 200 status)) + + (given body + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927"))))) diff --git a/modules/operation-value-set-expand/tests.edn b/modules/operation-value-set-expand/tests.edn new file mode 100644 index 000000000..12727a10d --- /dev/null +++ b/modules/operation-value-set-expand/tests.edn @@ -0,0 +1,10 @@ +#kaocha/v1 + #merge + [{} + #profile {:ci {:reporter kaocha.report/documentation + :color? false} + :coverage {:plugins [:kaocha.plugin/cloverage] + :cloverage/opts + {:codecov? true} + :reporter kaocha.report/documentation + :color? false}}] diff --git a/modules/rocksdb/test/blaze/db/kv/rocksdb/impl_test.clj b/modules/rocksdb/test/blaze/db/kv/rocksdb/impl_test.clj index 0c88dc910..cc28cef10 100644 --- a/modules/rocksdb/test/blaze/db/kv/rocksdb/impl_test.clj +++ b/modules/rocksdb/test/blaze/db/kv/rocksdb/impl_test.clj @@ -198,7 +198,7 @@ (.createColumnFamily ^RocksDB db (ColumnFamilyDescriptor. (from-hex name)))) (deftest put-wb-test - (with-open [db (RocksDB/open (str (new-temp-dir!)))] + (with-open [db (RocksDB/open (new-temp-dir!))] (let [cfh-1 (cfh db "01") cfh-2 (cfh db "02")] (are [entries state-val] (let [state (atom [])] @@ -234,7 +234,7 @@ (swap! state conj [cfh (to-hex key)])))) (deftest delete-wb-test - (with-open [db (RocksDB/open (str (new-temp-dir!)))] + (with-open [db (RocksDB/open (new-temp-dir!))] (let [cfh-1 (cfh db "01") cfh-2 (cfh db "02")] (are [entries state-val] (let [state (atom [])] @@ -264,7 +264,7 @@ (swap! state conj [cfh (to-hex key) (to-hex val)])))) (deftest write-wb-test - (with-open [db (RocksDB/open (str (new-temp-dir!)))] + (with-open [db (RocksDB/open (new-temp-dir!))] (let [cfh-1 (cfh db "01") cfh-2 (cfh db "02")] (testing "put" @@ -361,7 +361,7 @@ ::anom/message := "msg-151628"))) (deftest listener-test - (with-open [db (RocksDB/open (str (new-temp-dir!)))] + (with-open [db (RocksDB/open (new-temp-dir!))] (testing "compaction begin" (let [res (volatile! nil)] (.onCompactionBegin diff --git a/modules/terminology-service/.clj-kondo/config.edn b/modules/terminology-service/.clj-kondo/config.edn index 78c4a3953..295a4674d 100644 --- a/modules/terminology-service/.clj-kondo/config.edn +++ b/modules/terminology-service/.clj-kondo/config.edn @@ -1,2 +1,5 @@ {:config-paths - ["../../../.clj-kondo/root"]} + ["../../../.clj-kondo/root" + "../../async/resources/clj-kondo.exports/blaze/async" + "../../db-stub/resources/clj-kondo.exports/blaze/db-stub" + "../../module-test-util/resources/clj-kondo.exports/blaze/module-test-util"]} diff --git a/modules/terminology-service/Makefile b/modules/terminology-service/Makefile index edc7a4476..6ddac5bcb 100644 --- a/modules/terminology-service/Makefile +++ b/modules/terminology-service/Makefile @@ -1,21 +1,25 @@ fmt: - cljfmt check src deps.edn + cljfmt check src test deps.edn tests.edn lint: - clj-kondo --lint src deps.edn + clj-kondo --lint src test deps.edn -test: - true +prep: + clojure -X:deps prep -test-coverage: - true +test: prep + clojure -M:test:kaocha --profile :ci + +test-coverage: prep + clojure -M:test:coverage cloc-prod: cloc src cloc-test: + cloc test clean: rm -rf .clj-kondo/.cache .cpcache target -.PHONY: fmt lint test test-coverage cloc-prod cloc-test clean +.PHONY: fmt lint prep test test-coverage cloc-prod cloc-test clean diff --git a/modules/terminology-service/deps.edn b/modules/terminology-service/deps.edn index 5f95a31aa..d57beb4e5 100644 --- a/modules/terminology-service/deps.edn +++ b/modules/terminology-service/deps.edn @@ -1,3 +1,25 @@ {:deps - {blaze/async - {:local/root "../async"}}} + {blaze/db + {:local/root "../db"}} + + :aliases + {:test + {:extra-paths ["test"] + + :extra-deps + {blaze/db-stub + {:local/root "../db-stub"}}} + + :kaocha + {:extra-deps + {lambdaisland/kaocha + {:mvn/version "1.91.1392"}} + + :main-opts ["-m" "kaocha.runner"]} + + :coverage + {:extra-deps + {lambdaisland/kaocha-cloverage + {:mvn/version "1.1.89"}} + + :main-opts ["-m" "kaocha.runner" "--profile" "coverage"]}}} diff --git a/modules/terminology-service/src/blaze/terminology_service.clj b/modules/terminology-service/src/blaze/terminology_service.clj index f94d4a5e9..3c93def98 100644 --- a/modules/terminology-service/src/blaze/terminology_service.clj +++ b/modules/terminology-service/src/blaze/terminology_service.clj @@ -1,17 +1,10 @@ -(ns blaze.terminology-service) - -(defprotocol TerminologyService - (-expand-value-set [_ params])) - -(defn terminology-service? [x] - (satisfies? TerminologyService x)) +(ns blaze.terminology-service + (:require + [blaze.terminology-service.protocols :as p])) (defn expand-value-set - "Possible params are: - * url - * valueSetVersion - * filter - - See also: https://www.hl7.org/fhir/valueset-operation-expand.html" - [terminology-service params] - (-expand-value-set terminology-service params)) + "Returns a CompletableFuture that will complete with the expanded variant of + the ValueSet specified in `request` or will complete exceptionally with an + anomaly in case of an error." + [terminology-service request] + (p/-expand-value-set terminology-service request)) diff --git a/modules/terminology-service/src/blaze/terminology_service/local.clj b/modules/terminology-service/src/blaze/terminology_service/local.clj new file mode 100644 index 000000000..c3ab5d163 --- /dev/null +++ b/modules/terminology-service/src/blaze/terminology_service/local.clj @@ -0,0 +1,122 @@ +(ns blaze.terminology-service.local + (:require + [blaze.anomaly :as ba] + [blaze.async.comp :as ac :refer [do-sync]] + [blaze.coll.core :as coll] + [blaze.db.api :as d] + [blaze.fhir.spec.type :as type] + [blaze.module :as m] + [blaze.terminology-service :as ts] + [blaze.terminology-service.protocols :as p] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [taoensso.timbre :as log])) + +(defn- expand-complete-code-system [{:keys [url] concepts :concept}] + (mapv (fn [{:keys [code]}] {:system url :code code}) concepts)) + +(defn- expand-code-system [{:keys [url] concepts :concept} concepts-to-include] + (let [codes-to-include (into #{} (map :code) concepts-to-include)] + (into + [] + (keep + (fn [{:keys [code]}] + (when (contains? codes-to-include code) + {:system url :code code}))) + concepts))) + +(defn- code-system-not-found-msg [url] + (format "The CodeSystem with URL `%s` was not found." url)) + +(defn- code-system-not-found-anom [db id] + (ba/not-found (code-system-not-found-msg id) :t (d/t db))) + +(defn- find-code-system [db url] + (if-let [code-system (coll/first (d/type-query db "CodeSystem" [["url" url]]))] + (d/pull db code-system) + (ac/completed-future (code-system-not-found-anom db url)))) + +(defn- include-system [db {:keys [system] concepts :concept}] + (do-sync [code-system (find-code-system db (type/value system))] + (if (seq concepts) + (expand-code-system code-system concepts) + (expand-complete-code-system code-system)))) + +(declare expand-value-set-by-url) + +(defn- include-value-sets [db value-sets] + (let [futures (mapv #(expand-value-set-by-url db (type/value %)) value-sets)] + (do-sync [_ (ac/all-of futures)] + (transduce (map (comp :contains :expansion ac/join)) into futures)))) + +(defn- include [db {:keys [system] value-sets :valueSet :as include}] + (cond + system (include-system db include) + value-sets (include-value-sets db value-sets) + :else (ac/completed-future (ba/incorrect "Missing system or valueSet.")))) + +(defn- expand-includes [db includes] + (let [futures (mapv (partial include db) includes)] + (do-sync [_ (ac/all-of futures)] + (transduce (map ac/join) into futures)))) + +(defn remove-excludes-duplicates [includes excludes] + (into [] (comp (remove #(some (partial = %) excludes)) (distinct)) includes)) + +(defn- expand-value-set [db {{includes :include excludes :exclude} :compose}] + (let [includes (expand-includes db includes) + excludes (expand-includes db excludes)] + (do-sync [_ (ac/all-of [includes excludes])] + {:fhir/type :fhir/ValueSet + :expansion + {:fhir/type :fhir.ValueSet/expansion + :contains (remove-excludes-duplicates (ac/join includes) (ac/join excludes))}}))) + +(defn- value-set-not-found-by-id-msg [id] + (format "The ValueSet with id `%s` was not found." id)) + +(defn- value-set-not-found-by-id-anom [db id] + (ba/not-found (value-set-not-found-by-id-msg id) :t (d/t db))) + +(defn- non-deleted-resource-handle [db type id] + (when-let [handle (d/resource-handle db type id)] + (when-not (d/deleted? handle) + handle))) + +(defn- find-value-set-by-id [db id] + (if-let [value-set (non-deleted-resource-handle db "ValueSet" id)] + (d/pull db value-set) + (ac/completed-future (value-set-not-found-by-id-anom db id)))) + +(defn- value-set-not-found-by-url-msg [url] + (format "The ValueSet with URL `%s` was not found." url)) + +(defn- value-set-not-found-by-url-anom [db url] + (ba/not-found (value-set-not-found-by-url-msg url) :t (d/t db))) + +(defn- find-value-set-by-url [db url] + (if-let [value-set (coll/first (d/type-query db "ValueSet" [["url" url]]))] + (d/pull db value-set) + (ac/completed-future (value-set-not-found-by-url-anom db url)))) + +(defn- expand-value-set-by-url [db url] + (-> (find-value-set-by-url db url) + (ac/then-compose (partial expand-value-set db)))) + +(defmethod m/pre-init-spec ::ts/local [_] + (s/keys)) + +(defmethod ig/init-key ::ts/local + [_ _] + (log/info "Init local terminology server") + (reify p/TerminologyService + (-expand-value-set [_ {:keys [db id url]}] + (if db + (cond + id (-> (find-value-set-by-id db id) + (ac/then-compose (partial expand-value-set db))) + url (expand-value-set-by-url db url) + :else (ac/completed-future (ba/incorrect "Missing ID or URL."))) + (ac/completed-future (ba/incorrect "Missing database.")))))) + +(derive ::ts/local :blaze/terminology-service) diff --git a/modules/terminology-service/src/blaze/terminology_service/protocols.clj b/modules/terminology-service/src/blaze/terminology_service/protocols.clj new file mode 100644 index 000000000..46ba3e419 --- /dev/null +++ b/modules/terminology-service/src/blaze/terminology_service/protocols.clj @@ -0,0 +1,4 @@ +(ns blaze.terminology-service.protocols) + +(defprotocol TerminologyService + (-expand-value-set [_ request])) diff --git a/modules/terminology-service/src/blaze/terminology_service/spec.clj b/modules/terminology-service/src/blaze/terminology_service/spec.clj new file mode 100644 index 000000000..fe95ca8fc --- /dev/null +++ b/modules/terminology-service/src/blaze/terminology_service/spec.clj @@ -0,0 +1,19 @@ +(ns blaze.terminology-service.spec + (:require + [blaze.db.spec] + [blaze.terminology-service.expand-value-set :as-alias expand-vs] + [blaze.terminology-service.expand-value-set.request :as-alias request] + [blaze.terminology-service.protocols :as p] + [clojure.spec.alpha :as s])) + +(defn terminology-service? [x] + (satisfies? p/TerminologyService x)) + +(s/def :blaze/terminology-service + terminology-service?) + +(s/def ::request/url + string?) + +(s/def ::expand-vs/request + (s/keys :opt-un [:blaze.db/db ::request/url])) diff --git a/modules/terminology-service/src/blaze/terminology_service_spec.clj b/modules/terminology-service/src/blaze/terminology_service_spec.clj index dd320e91d..e88de8814 100644 --- a/modules/terminology-service/src/blaze/terminology_service_spec.clj +++ b/modules/terminology-service/src/blaze/terminology_service_spec.clj @@ -1,21 +1,12 @@ (ns blaze.terminology-service-spec (:require [blaze.async.comp :as ac] - [blaze.terminology-service :as terminology-service :refer [terminology-service?]] + [blaze.terminology-service :as ts] + [blaze.terminology-service.expand-value-set :as-alias expand-vs] + [blaze.terminology-service.spec] [clojure.spec.alpha :as s])) -(s/def ::url - string?) - -(s/def ::valueSetVersion - string?) - -(s/def ::filter - string?) - -(def expand-value-set-params - (s/keys :opt-un [::url ::valueSetVersion ::filter])) - -(s/fdef terminology-service/expand-value-set - :args (s/cat :terminology-service terminology-service? :params expand-value-set-params) +(s/fdef ts/expand-value-set + :args (s/cat :terminology-service :blaze/terminology-service + :request ::expand-vs/request) :ret ac/completable-future?) diff --git a/modules/terminology-service/test/blaze/terminology_service/local_test.clj b/modules/terminology-service/test/blaze/terminology_service/local_test.clj new file mode 100644 index 000000000..a251104ae --- /dev/null +++ b/modules/terminology-service/test/blaze/terminology_service/local_test.clj @@ -0,0 +1,408 @@ +(ns blaze.terminology-service.local-test + (:require + [blaze.db.api :as d] + [blaze.db.api-stub :refer [mem-node-config with-system-data]] + [blaze.fhir.test-util] + [blaze.module.test-util :refer [given-failed-future with-system]] + [blaze.terminology-service :as ts] + [blaze.terminology-service-spec] + [blaze.terminology-service.local] + [blaze.test-util :as tu :refer [given-thrown]] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest testing]] + [cognitect.anomalies :as anom] + [integrant.core :as ig] + [juxt.iota :refer [given]] + [taoensso.timbre :as log])) + +(set! *warn-on-reflection* true) +(st/instrument) +(log/set-min-level! :trace) + +(test/use-fixtures :each tu/fixture) + +(deftest init-test + (testing "nil config" + (given-thrown (ig/init {::ts/local nil}) + :key := ::ts/local + :reason := ::ig/build-failed-spec + [:cause-data ::s/problems 0 :pred] := `map?))) + +(def config + (assoc mem-node-config ::ts/local {})) + +(deftest terminology-service-test + (with-system [{ts ::ts/local :blaze.db/keys [node]} config] + + (testing "missing database" + (given-failed-future (ts/expand-value-set ts {}) + ::anom/category := ::anom/incorrect + ::anom/message := "Missing database.")) + + (testing "missing id or url" + (given-failed-future (ts/expand-value-set ts {:db (d/db node)}) + ::anom/category := ::anom/incorrect + ::anom/message := "Missing ID or URL.")) + + (testing "not found" + (testing "id" + (given-failed-future (ts/expand-value-set ts {:db (d/db node) :id "id-175736"}) + ::anom/category := ::anom/not-found + ::anom/message := "The ValueSet with id `id-175736` was not found." + :t := 0)) + + (testing "url" + (given-failed-future (ts/expand-value-set ts {:db (d/db node) :url "url-194718"}) + ::anom/category := ::anom/not-found + ::anom/message := "The ValueSet with URL `url-194718` was not found." + :t := 0)))) + + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/ValueSet :id "id-180012"}]] + [[:delete "ValueSet" "id-180012"]]] + + (testing "deleted ValueSet not found" + (given-failed-future (ts/expand-value-set ts {:db (d/db node) :id "id-180012"}) + ::anom/category := ::anom/not-found + ::anom/message := "The ValueSet with id `id-180012` was not found." + :t := 2))) + + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (testing "code system not found" + (given-failed-future (ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + ::anom/category := ::anom/not-found + ::anom/message := "The CodeSystem with URL `system-115910` was not found." + :t := 1))) + + (testing "with one code system" + (testing "with one code" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (doseq [request [{:url "value-set-135750"} {:id "0"}]] + (given @(ts/expand-value-set ts (assoc request :db (d/db node))) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927")))) + + (testing "with two codes" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163444"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 2 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927" + [:expansion :contains 1 :system] := #fhir/uri"system-115910" + [:expansion :contains 1 :code] := #fhir/code"code-163444")) + + (testing "include only one code" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163444"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.ValueSet.compose.include/concept + :code #fhir/code"code-163444"}]}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-163444"))) + + (testing "exclude one code" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163444"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"}] + :exclude + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.ValueSet.compose.include/concept + :code #fhir/code"code-163444"}]}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927"))))) + + (testing "with two code systems" + (testing "with one code each" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"}]}] + [:put {:fhir/type :fhir/CodeSystem :id "1" + :url #fhir/uri"system-180814" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-180828"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"} + {:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 2 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927" + [:expansion :contains 1 :system] := #fhir/uri"system-180814" + [:expansion :contains 1 :code] := #fhir/code"code-180828"))) + + (testing "with two codes each" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163824"}]}] + [:put {:fhir/type :fhir/CodeSystem :id "1" + :url #fhir/uri"system-180814" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-180828"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163852"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"} + {:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 4 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927" + [:expansion :contains 1 :system] := #fhir/uri"system-115910" + [:expansion :contains 1 :code] := #fhir/code"code-163824" + [:expansion :contains 2 :system] := #fhir/uri"system-180814" + [:expansion :contains 2 :code] := #fhir/code"code-180828" + [:expansion :contains 3 :system] := #fhir/uri"system-180814" + [:expansion :contains 3 :code] := #fhir/code"code-163852")) + + (testing "excluding the second code from the first code system" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-115927"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163824"}]}] + [:put {:fhir/type :fhir/CodeSystem :id "1" + :url #fhir/uri"system-180814" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-180828"} + {:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-163852"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910"} + {:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}] + :exclude + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-115910" + :concept + [{:fhir/type :fhir.ValueSet.compose.include/concept + :code #fhir/code"code-163824"}]}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-135750"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 3 + [:expansion :contains 0 :system] := #fhir/uri"system-115910" + [:expansion :contains 0 :code] := #fhir/code"code-115927" + [:expansion :contains 1 :system] := #fhir/uri"system-180814" + [:expansion :contains 1 :code] := #fhir/code"code-180828" + [:expansion :contains 2 :system] := #fhir/uri"system-180814" + [:expansion :contains 2 :code] := #fhir/code"code-163852"))))) + + (testing "with value set refs" + (testing "one value set ref" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-180814" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-180828"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}]}}] + [:put {:fhir/type :fhir/ValueSet :id "1" + :url #fhir/uri"value-set-161213" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :valueSet [#fhir/canonical"value-set-135750"]}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-161213"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-180814" + [:expansion :contains 0 :code] := #fhir/code"code-180828"))) + + (testing "two value set refs" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-180814" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-180828"}]}] + [:put {:fhir/type :fhir/CodeSystem :id "1" + :url #fhir/uri"system-162531" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-162551"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}]}}] + [:put {:fhir/type :fhir/ValueSet :id "1" + :url #fhir/uri"value-set-162451" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-162531"}]}}] + [:put {:fhir/type :fhir/ValueSet :id "2" + :url #fhir/uri"value-set-162456" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :valueSet + [#fhir/canonical"value-set-135750" + #fhir/canonical"value-set-162451"]}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-162456"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 2 + [:expansion :contains 0 :system] := #fhir/uri"system-180814" + [:expansion :contains 0 :code] := #fhir/code"code-180828" + [:expansion :contains 1 :system] := #fhir/uri"system-162531" + [:expansion :contains 1 :code] := #fhir/code"code-162551"))) + + (testing "two value set refs including the same code system" + (with-system-data [{ts ::ts/local :blaze.db/keys [node]} config] + [[[:put {:fhir/type :fhir/CodeSystem :id "0" + :url #fhir/uri"system-180814" + :concept + [{:fhir/type :fhir.CodeSystem/concept + :code #fhir/code"code-180828"}]}] + [:put {:fhir/type :fhir/ValueSet :id "0" + :url #fhir/uri"value-set-135750" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}]}}] + [:put {:fhir/type :fhir/ValueSet :id "1" + :url #fhir/uri"value-set-162451" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :system #fhir/uri"system-180814"}]}}] + [:put {:fhir/type :fhir/ValueSet :id "2" + :url #fhir/uri"value-set-162456" + :compose + {:fhir/type :fhir.ValueSet/compose + :include + [{:fhir/type :fhir.ValueSet.compose/include + :valueSet + [#fhir/canonical"value-set-135750" + #fhir/canonical"value-set-162451"]}]}}]]] + + (given @(ts/expand-value-set ts {:db (d/db node) :url "value-set-162456"}) + :fhir/type := :fhir/ValueSet + [:expansion :contains count] := 1 + [:expansion :contains 0 :system] := #fhir/uri"system-180814" + [:expansion :contains 0 :code] := #fhir/code"code-180828"))))) diff --git a/modules/terminology-service/tests.edn b/modules/terminology-service/tests.edn new file mode 100644 index 000000000..0b8674ba5 --- /dev/null +++ b/modules/terminology-service/tests.edn @@ -0,0 +1,11 @@ +#kaocha/v1 + #merge + [{} + #profile {:ci {:reporter kaocha.report/documentation + :color? false} + :coverage {:plugins [:kaocha.plugin/cloverage] + :cloverage/opts + {:ns-exclude-regex [".+\\.spec"], + :codecov? true} + :reporter kaocha.report/documentation + :color? false}}] diff --git a/resources/blaze.edn b/resources/blaze.edn index 0cc5b8e76..1221e864a 100644 --- a/resources/blaze.edn +++ b/resources/blaze.edn @@ -110,7 +110,15 @@ :affects-state false :resource-types ["Resource"] :system-handler #blaze/ref :blaze.fhir.operation/totals - :documentation "Retrieves the total counts of resources available by resource type."}] + :documentation "Retrieves the total counts of resources available by resource type."} + #:blaze.rest-api.operation + {:code "expand" + :def-uri "http://hl7.org/fhir/OperationDefinition/ValueSet-expand" + :affects-state false + :resource-types ["ValueSet"] + :type-handler #blaze/ref :blaze.fhir.operation.value-set/expand + :instance-handler #blaze/ref :blaze.fhir.operation.value-set/expand + :documentation "The $expand operation can be used to expand all codes of a ValueSet."}] ;; ;; FHIR Interactions @@ -222,6 +230,7 @@ ;; :blaze.fhir.operation.evaluate-measure/handler {:node #blaze/ref :blaze.db.main/node + :terminology-service #blaze/ref :blaze/terminology-service :executor #blaze/ref :blaze.fhir.operation.evaluate-measure/executor :clock #blaze/ref :blaze/clock :rng-fn #blaze/ref :blaze/rng-fn @@ -263,6 +272,15 @@ :blaze.fhir.operation/totals {:structure-definition-repo #blaze/ref :blaze.fhir/structure-definition-repo} + ;; + ;; FHIR Operation ValueSet Expand + ;; + :blaze.fhir.operation.value-set/expand + {:terminology-service #blaze/ref :blaze/terminology-service} + + :blaze.terminology-service/local + {} + ;; ;; Admin API ;;