From 2b9ecd6308688cb52c5947c44f19f199ac8c0fde Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Tue, 14 May 2024 17:48:13 +0200 Subject: [PATCH] Lower Memory Consumption while Evaluating Stratifiers Closes: #1723 --- .../scripts/cql/stratifier-condition-code.cql | 11 + .../scripts/cql/stratifier-condition-code.csv | 203 +++++++++++ .../scripts/cql/stratifier-condition-code.yml | 8 + .../evaluate-measure-blazectl-stratifier.sh | 2 +- .github/workflows/build.yml | 3 + docs/performance/cql.md | 16 + .../cql/observation-code-search.sh | 18 - .../cql/stratifier-condition-code.cql | 11 + .../cql/stratifier-condition-code.yml | 8 + ...stratifier-observation-laboratory-code.cql | 13 + ...stratifier-observation-laboratory-code.yml | 8 + .../src/blaze/elm/compiler/library/spec.clj | 40 +++ .../src/blaze/elm/compiler/library_spec.clj | 21 +- .../src/blaze/db/resource_store/kv.clj | 31 +- modules/db/src/blaze/db/tx_log/local.clj | 2 +- .../src/blaze/fhir/spec/spec.clj | 15 + .../src/blaze/interaction/transaction.clj | 7 +- .../src/blaze/interaction/util.clj | 4 +- modules/luid/src/blaze/luid.clj | 32 +- modules/luid/src/blaze/luid/spec.clj | 7 + modules/luid/src/blaze/luid_spec.clj | 17 +- modules/luid/test/blaze/luid_test.clj | 21 +- .../fhir/operation/evaluate_measure/cql.clj | 307 +++++----------- .../operation/evaluate_measure/measure.clj | 204 +++++++---- .../evaluate_measure/measure/group.clj | 164 +++++++++ .../evaluate_measure/measure/population.clj | 38 +- .../evaluate_measure/measure/stratifier.clj | 244 ++++++++----- .../evaluate_measure/measure/util.clj | 49 +-- .../operation/evaluate_measure/cql/spec.clj | 30 +- .../operation/evaluate_measure/cql_spec.clj | 43 ++- .../operation/evaluate_measure/cql_test.clj | 327 +++++++---------- .../evaluate_measure/measure/group_spec.clj | 29 ++ .../measure/population/spec.clj | 9 +- .../measure/population_spec.clj | 5 + .../measure/stratifier/spec.clj | 10 +- .../measure/stratifier_spec.clj | 34 +- .../measure/stratifier_test.clj | 335 +++++++----------- .../evaluate_measure/measure/util_spec.clj | 15 +- .../evaluate_measure/measure/util_test.clj | 26 +- .../evaluate_measure/measure_test.clj | 332 ++++++++++------- .../q54-stratifier-condition-code.cql | 11 + .../q54-stratifier-condition-code.json | 157 ++++++++ .../q55-stratifier-bmi-observation.cql | 13 + .../q55-stratifier-bmi-observation.json | 111 ++++++ .../q56-stratifier-observation-code-value.cql | 16 + ...q56-stratifier-observation-code-value.json | 154 ++++++++ .../fhir/operation/evaluate_measure_test.clj | 8 +- 47 files changed, 2026 insertions(+), 1143 deletions(-) create mode 100644 .github/scripts/cql/stratifier-condition-code.cql create mode 100644 .github/scripts/cql/stratifier-condition-code.csv create mode 100644 .github/scripts/cql/stratifier-condition-code.yml delete mode 100755 docs/performance/cql/observation-code-search.sh create mode 100644 docs/performance/cql/stratifier-condition-code.cql create mode 100644 docs/performance/cql/stratifier-condition-code.yml create mode 100644 docs/performance/cql/stratifier-observation-laboratory-code.cql create mode 100644 docs/performance/cql/stratifier-observation-laboratory-code.yml create mode 100644 modules/cql/src/blaze/elm/compiler/library/spec.clj create mode 100644 modules/luid/src/blaze/luid/spec.clj create mode 100644 modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/group.clj create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/group_spec.clj create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.cql create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.json create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.cql create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.json create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.cql create mode 100644 modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.json diff --git a/.github/scripts/cql/stratifier-condition-code.cql b/.github/scripts/cql/stratifier-condition-code.cql new file mode 100644 index 000000000..a743b2997 --- /dev/null +++ b/.github/scripts/cql/stratifier-condition-code.cql @@ -0,0 +1,11 @@ +library "stratifier-condition-code" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +context Patient + +define InInitialPopulation: + [Condition] + +define function Code(condition FHIR.Condition): + condition.code diff --git a/.github/scripts/cql/stratifier-condition-code.csv b/.github/scripts/cql/stratifier-condition-code.csv new file mode 100644 index 000000000..fd76c465c --- /dev/null +++ b/.github/scripts/cql/stratifier-condition-code.csv @@ -0,0 +1,203 @@ +"Acquired coagulation disorder (disorder)",1 +"Acute Cholecystitis",3 +"Acute allergic reaction",12 +"Acute bacterial sinusitis (disorder)",70 +"Acute bronchitis (disorder)",527 +"Acute deep venous thrombosis (disorder)",9 +"Acute myeloid leukemia, disease (disorder)",103 +"Acute pulmonary embolism (disorder)",6 +"Acute respiratory distress syndrome (disorder)",2 +"Acute respiratory failure (disorder)",2 +"Acute viral pharyngitis (disorder)",598 +"Alcoholism",8 +"Alzheimer's disease (disorder)",50 +"Anemia (disorder)",382 +"Antepartum eclampsia",18 +"Appendicitis",54 +"Asthma",1 +"Atopic dermatitis",13 +"Atrial Fibrillation",95 +"Bacteremia (finding)",22 +"Bleeding from anus",1 +"Blighted ovum",16 +"Blindness due to type 2 diabetes mellitus (disorder)",1 +"Body mass index 30+ - obesity (finding)",528 +"Body mass index 40+ - severely obese (finding)",8 +"Brain damage - traumatic",10 +"Bullet wound",1 +"COVID-19",83 +"Carcinoma in situ of prostate (disorder)",29 +"Cardiac Arrest",44 +"Child attention deficit disorder",20 +"Childhood asthma",18 +"Chill (finding)",15 +"Cholelithiasis",3 +"Chronic congestive heart failure (disorder)",9 +"Chronic intractable migraine without aura",49 +"Chronic kidney disease stage 1 (disorder)",148 +"Chronic kidney disease stage 2 (disorder)",137 +"Chronic kidney disease stage 3 (disorder)",96 +"Chronic kidney disease stage 4 (disorder)",18 +"Chronic low back pain (finding)",199 +"Chronic neck pain (finding)",131 +"Chronic obstructive bronchitis (disorder)",12 +"Chronic pain",52 +"Chronic paralysis due to lesion of spinal cord",2 +"Chronic sinusitis (disorder)",257 +"Chronic type B viral hepatitis (disorder)",1 +"Closed fracture of hip",16 +"Concussion injury of brain",4 +"Concussion with loss of consciousness",14 +"Concussion with no loss of consciousness",49 +"Contact dermatitis",4 +"Coronary Heart Disease",90 +"Cough (finding)",52 +"Cystitis",21 +"Diabetes",96 +"Diabetic renal disease (disorder)",157 +"Diabetic retinopathy associated with type II diabetes mellitus (disorder)",25 +"Diarrhea symptom (finding)",3 +"Drug overdose",62 +"Dyspnea (finding)",24 +"Epilepsy",13 +"Escherichia coli urinary tract infection",25 +"Facial laceration",36 +"Familial Alzheimer's disease of early onset (disorder)",5 +"Fatigue (finding)",40 +"Febrile neutropenia (disorder)",79 +"Fetus with unknown complication",26 +"Fever (finding)",77 +"Fibromyalgia (disorder)",22 +"First degree burn",26 +"Fracture of ankle",32 +"Fracture of clavicle",39 +"Fracture of forearm",60 +"Fracture of rib",20 +"Fracture of vertebral column without spinal cord injury",2 +"Fracture subluxation of wrist",44 +"Full-time employment (finding)",20900 +"Gout",9 +"Has a criminal record (finding)",312 +"Headache (finding)",14 +"History of amputation of foot (situation)",1 +"History of appendectomy",54 +"History of cardiac arrest (situation)",44 +"History of disarticulation at wrist (situation)",1 +"History of lower limb amputation (situation)",2 +"History of myocardial infarction (situation)",5 +"History of single seizure (situation)",25 +"Homeless (finding)",7 +"Housing unsatisfactory (finding)",113 +"Human immunodeficiency virus infection (disorder)",2 +"Hyperglycemia (disorder)",42 +"Hyperlipidemia",158 +"Hypertension",466 +"Hypertriglyceridemia (disorder)",97 +"Hypoxemia (disorder)",15 +"Idiopathic atrophic hypothyroidism",13 +"Impacted molars",55 +"Injury of anterior cruciate ligament",3 +"Injury of medial collateral ligament of knee",16 +"Injury of tendon of the rotator cuff of shoulder",11 +"Joint pain (finding)",17 +"Laceration of foot",43 +"Laceration of forearm",28 +"Laceration of hand",20 +"Laceration of thigh",31 +"Lack of access to transportation (finding)",96 +"Limited social contact (finding)",1514 +"Localized, primary osteoarthritis of the hand",37 +"Loss of taste (finding)",43 +"Lupus erythematosus",1 +"Macular edema and retinopathy due to type 2 diabetes mellitus (disorder)",6 +"Major depression disorder",3 +"Malignant neoplasm of breast (disorder)",25 +"Malignant tumor of colon",3 +"Metabolic syndrome X (disorder)",244 +"Metastasis from malignant tumor of prostate (disorder)",2 +"Microalbuminuria due to type 2 diabetes mellitus (disorder)",138 +"Miscarriage in first trimester",220 +"Misuses drugs (finding)",60 +"Muscle pain (finding)",17 +"Myocardial Infarction",5 +"Nausea (finding)",5 +"Neoplasm of prostate",31 +"Neuropathy due to type 2 diabetes mellitus (disorder)",37 +"Neutropenia (disorder)",24 +"Nonproliferative diabetic retinopathy due to type 2 diabetes mellitus (disorder)",10 +"Normal pregnancy",370 +"Not in labor force (finding)",1513 +"Obstructive sleep apnea syndrome (disorder)",38 +"Only received primary school education (finding)",85 +"Opioid abuse (disorder)",11 +"Osteoarthritis of hip",30 +"Osteoarthritis of knee",58 +"Osteoporosis (disorder)",129 +"Otitis media",145 +"Overlapping malignant neoplasm of colon",5 +"Part-time employment (finding)",3496 +"Passive conjunctival congestion (finding)",3 +"Pathological fracture due to osteoporosis (disorder)",61 +"Perennial allergic rhinitis with seasonal variation",16 +"Perennial allergic rhinitis",17 +"Pneumonia (disorder)",15 +"Polyp of colon",101 +"Posttraumatic stress disorder",1 +"Prediabetes",444 +"Preeclampsia",19 +"Primary fibromyalgia syndrome",7 +"Primary malignant neoplasm of colon",4 +"Proliferative diabetic retinopathy due to type II diabetes mellitus (disorder)",4 +"Proteinuria due to type 2 diabetes mellitus (disorder)",96 +"Protracted diarrhea",1 +"Pulmonary emphysema (disorder)",9 +"Received certificate of high school equivalency (finding)",260 +"Received higher education (finding)",490 +"Recurrent rectal polyp",28 +"Recurrent urinary tract infection",13 +"Refugee (person)",53 +"Reports of violence in the environment (finding)",756 +"Respiratory distress (finding)",15 +"Rheumatoid arthritis",1 +"Risk activity involvement (finding)",333 +"Rupture of appendix",20 +"Rupture of patellar tendon",8 +"Seasonal allergic rhinitis",5 +"Second degree burn",16 +"Secondary malignant neoplasm of colon",1 +"Seizure disorder",25 +"Sepsis (disorder)",27 +"Sepsis caused by virus (disorder)",2 +"Septic shock (disorder)",2 +"Served in armed forces (finding)",52 +"Severe anxiety (panic) (finding",181 +"Shock (disorder)",1 +"Sinusitis (disorder)",64 +"Sleep apnea (disorder)",10 +"Sleep disorder (disorder)",48 +"Smokes tobacco daily",12 +"Social isolation (finding)",1677 +"Social migrant (finding)",2 +"Sore throat symptom (finding)",11 +"Sprain of ankle",109 +"Sprain of wrist",49 +"Sputum finding (finding)",27 +"Streptococcal sore throat (disorder)",161 +"Stress (finding)",7477 +"Stroke",110 +"Suspected COVID-19",84 +"Suspected lung cancer (situation)",1 +"Tear of meniscus of knee",6 +"Third degree burn",2 +"Toxoplasma gondii antibody positive (finding)",1 +"Transformed migraine (disorder)",14 +"Transport problems (finding)",95 +"Traumatic brain injury (disorder)",1 +"Tubal pregnancy",36 +"Unemployed (finding)",76 +"Unhealthy alcohol drinking behavior (finding)",169 +"Victim of intimate partner abuse (finding)",1184 +"Viral sinusitis (disorder)",1083 +"Vomiting symptom (finding)",5 +"Wheezing (finding)",24 +"Whiplash injury to neck",42 diff --git a/.github/scripts/cql/stratifier-condition-code.yml b/.github/scripts/cql/stratifier-condition-code.yml new file mode 100644 index 000000000..e795b20b7 --- /dev/null +++ b/.github/scripts/cql/stratifier-condition-code.yml @@ -0,0 +1,8 @@ +library: .github/scripts/cql/stratifier-condition-code.cql +group: +- type: Condition + population: + - expression: InInitialPopulation + stratifier: + - code: code + expression: Code diff --git a/.github/scripts/evaluate-measure-blazectl-stratifier.sh b/.github/scripts/evaluate-measure-blazectl-stratifier.sh index 219224cdd..c6c1df0cf 100755 --- a/.github/scripts/evaluate-measure-blazectl-stratifier.sh +++ b/.github/scripts/evaluate-measure-blazectl-stratifier.sh @@ -14,7 +14,7 @@ else exit 1 fi -STRATIFIER_DATA=$(echo "$REPORT" | jq -r '.group[0].stratifier[0].stratum[] | [.value.text, .population[0].count] | @csv') +STRATIFIER_DATA=$(echo "$REPORT" | jq -r '.group[0].stratifier[0].stratum[] | [.value.text, .population[0].count] | @csv' | sort) EXPECTED_STRATIFIER_DATA=$(cat ".github/scripts/cql/$NAME.csv") if [ "$STRATIFIER_DATA" = "$EXPECTED_STRATIFIER_DATA" ]; then diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0115e122..4246c6f04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -829,6 +829,9 @@ jobs: - name: Count the number of Patients with Stress Conditions at Inpatient Encounters using CQL run: .github/scripts/evaluate-measure-blazectl.sh inpatient-stress 13 + - name: Condition Code Stratifier + run: .github/scripts/evaluate-measure-blazectl-stratifier.sh stratifier-condition-code 51599 + - name: Patient Everything Too Costly run: .github/scripts/patient-everything-too-costly.sh diff --git a/docs/performance/cql.md b/docs/performance/cql.md index 673727de6..904fe4d42 100644 --- a/docs/performance/cql.md +++ b/docs/performance/cql.md @@ -382,3 +382,19 @@ define InInitialPopulation: ```sh cql/search.sh condition-ten-rare ``` + +## Condition Code Stratification + +### Data + +| Dataset | System | # Hits | Time (s) | StdDev | Pat./s | +|---------|--------|-------:|---------:|-------:|-------:| +| 1M | LEA58 | 52.3 M | 399.64 | 11.966 | 2.5 k | + +## Laboratory Observation Code Stratification + +### Data + +| Dataset | System | # Hits | Time (s) | StdDev | Pat./s | +|---------|--------|-------:|---------:|-------:|-------:| +| 100k | LEA58 | 37.8 M | 280.40 | 3.026 | 0 | diff --git a/docs/performance/cql/observation-code-search.sh b/docs/performance/cql/observation-code-search.sh deleted file mode 100755 index 4cbec618b..000000000 --- a/docs/performance/cql/observation-code-search.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -e - -BASE="http://localhost:8080/fhir" -START_EPOCH="$(date +"%s")" - -for CODE in "laboratory" -do - echo "Counting Observations with code $CODE..." - for i in {0..50} - do - blazectl evaluate-measure "cql/observation-category-$CODE.yml" --server "$BASE" 2> /dev/null |\ - jq -rf cql/duration.jq >> "$START_EPOCH-$CODE.times" - done - - # Skip first line because it will not benefit from caching - tail -n +2 "$START_EPOCH-$CODE.times" |\ - awk '{sum += $1; sumsq += $1^2} END {printf("Avg : %.3f\nStdDev : %.3f\n", sum/NR, sqrt(sumsq/NR - (sum/NR)^2))}' -done diff --git a/docs/performance/cql/stratifier-condition-code.cql b/docs/performance/cql/stratifier-condition-code.cql new file mode 100644 index 000000000..a743b2997 --- /dev/null +++ b/docs/performance/cql/stratifier-condition-code.cql @@ -0,0 +1,11 @@ +library "stratifier-condition-code" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +context Patient + +define InInitialPopulation: + [Condition] + +define function Code(condition FHIR.Condition): + condition.code diff --git a/docs/performance/cql/stratifier-condition-code.yml b/docs/performance/cql/stratifier-condition-code.yml new file mode 100644 index 000000000..1644a280d --- /dev/null +++ b/docs/performance/cql/stratifier-condition-code.yml @@ -0,0 +1,8 @@ +library: cql/stratifier-condition-code.cql +group: +- type: Condition + population: + - expression: InInitialPopulation + stratifier: + - code: code + expression: Code diff --git a/docs/performance/cql/stratifier-observation-laboratory-code.cql b/docs/performance/cql/stratifier-observation-laboratory-code.cql new file mode 100644 index 000000000..9fb8b11a5 --- /dev/null +++ b/docs/performance/cql/stratifier-observation-laboratory-code.cql @@ -0,0 +1,13 @@ +library "stratifier-observation-laboratory-code" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +codesystem category: 'http://terminology.hl7.org/CodeSystem/observation-category' + +context Patient + +define InInitialPopulation: + [Observation: category ~ Code 'laboratory' from category] + +define function Code(observation FHIR.Observation): + observation.code diff --git a/docs/performance/cql/stratifier-observation-laboratory-code.yml b/docs/performance/cql/stratifier-observation-laboratory-code.yml new file mode 100644 index 000000000..fe45e6a22 --- /dev/null +++ b/docs/performance/cql/stratifier-observation-laboratory-code.yml @@ -0,0 +1,8 @@ +library: cql/stratifier-observation-laboratory-code.cql +group: +- type: Observation + population: + - expression: InInitialPopulation + stratifier: + - code: code + expression: Code diff --git a/modules/cql/src/blaze/elm/compiler/library/spec.clj b/modules/cql/src/blaze/elm/compiler/library/spec.clj new file mode 100644 index 000000000..3ceccaddd --- /dev/null +++ b/modules/cql/src/blaze/elm/compiler/library/spec.clj @@ -0,0 +1,40 @@ +(ns blaze.elm.compiler.library.spec + (:require + [blaze.anomaly-spec] + [blaze.elm.compiler :as-alias c] + [blaze.elm.compiler-spec] + [blaze.elm.compiler.expression-def :as-alias expression-def] + [blaze.elm.compiler.function-def :as-alias function-def] + [blaze.elm.compiler.spec] + [blaze.fhir.spec.spec] + [clojure.spec.alpha :as s])) + +(s/def ::expression-def/name + string?) + +(s/def ::expression-def/context + :fhir.resource/type) + +(s/def ::c/expression-def + (s/keys :req-un [::expression-def/name ::expression-def/context ::c/expression])) + +(s/def ::c/expression-defs + (s/map-of :elm/name ::c/expression-def)) + +(s/def ::function-def/name + string?) + +(s/def ::function-def/context + :fhir.resource/type) + +(s/def ::c/function-def + (s/keys :req-un [::function-def/name ::function-def/context ::c/function])) + +(s/def ::c/function-defs + (s/map-of :elm/name ::c/function-def)) + +(s/def ::c/parameter-default-values + (s/map-of :elm/name ::c/expression)) + +(s/def ::c/library + (s/keys :req-un [::c/expression-defs ::c/function-defs ::c/parameter-default-values])) diff --git a/modules/cql/src/blaze/elm/compiler/library_spec.clj b/modules/cql/src/blaze/elm/compiler/library_spec.clj index 99fd348e9..d1318e391 100644 --- a/modules/cql/src/blaze/elm/compiler/library_spec.clj +++ b/modules/cql/src/blaze/elm/compiler/library_spec.clj @@ -3,32 +3,13 @@ [blaze.anomaly-spec] [blaze.elm.compiler :as-alias c] [blaze.elm.compiler-spec] - [blaze.elm.compiler.core :as core] - [blaze.elm.compiler.expression-def :as-alias expression-def] [blaze.elm.compiler.library :as library] + [blaze.elm.compiler.library.spec] [blaze.elm.compiler.spec] [blaze.fhir.spec.spec] [clojure.spec.alpha :as s] [cognitect.anomalies :as anom])) -(s/def ::expression-def/name - string?) - -(s/def ::expression-def/context - :fhir.resource/type) - -(s/def ::c/expression-def - (s/keys :req-un [::expression-def/name ::expression-def/context ::c/expression])) - -(s/def ::c/expression-defs - (s/map-of :elm/name core/expr?)) - -(s/def ::c/parameter-default-values - (s/map-of :elm/name ::c/expression)) - -(s/def ::c/library - (s/keys :req-un [::c/expression-defs ::c/parameter-default-values])) - (s/fdef library/compile-library :args (s/cat :node :blaze.db/node :library :elm/library :opts map?) :ret (s/or :library ::c/library :anomaly ::anom/anomaly)) diff --git a/modules/db-resource-store/src/blaze/db/resource_store/kv.clj b/modules/db-resource-store/src/blaze/db/resource_store/kv.clj index 404f4844c..40ddadbf1 100644 --- a/modules/db-resource-store/src/blaze/db/resource_store/kv.clj +++ b/modules/db-resource-store/src/blaze/db/resource_store/kv.clj @@ -37,32 +37,33 @@ (take 16 (iterate #(* 2 %) 1e-6)) "op") -(defn- parse-msg [hash cause-msg] +(defn- parse-msg [cause-msg hash] (format "Error while parsing resource content with hash `%s`: %s" hash cause-msg)) +(defn- parse-anom [e hash] + (-> (update e ::anom/message parse-msg hash) + (assoc :blaze.resource/hash hash))) + (defn- parse-cbor [bytes hash] - (-> (fhir-spec/parse-cbor bytes) - (ba/exceptionally - #(assoc % - ::anom/message (parse-msg hash (::anom/message %)) - :blaze.resource/hash hash)))) + (with-open [_ (prom/timer duration-seconds "parse-resource")] + (-> (fhir-spec/parse-cbor bytes) + (ba/exceptionally #(parse-anom % hash))))) (defn- conform-msg [hash] (format "Error while conforming resource content with hash `%s`." hash)) +(defn- conform-anom [_e hash] + (ba/fault (conform-msg hash) :blaze.resource/hash hash)) + (defn- conform-cbor [x hash] - (-> (fhir-spec/conform-cbor x) - (ba/exceptionally - (fn [_] - (ba/fault - (conform-msg hash) - :blaze.resource/hash hash))))) + (with-open [_ (prom/timer duration-seconds "conform-resource")] + (-> (fhir-spec/conform-cbor x) + (ba/exceptionally #(conform-anom % hash))))) (defn- parse-and-conform-cbor [bytes hash] - (with-open [_ (prom/timer duration-seconds "parse-resource")] - (when-ok [x (parse-cbor bytes hash)] - (conform-cbor x hash)))) + (when-ok [x (parse-cbor bytes hash)] + (conform-cbor x hash))) (def ^:private entry-freezer (map diff --git a/modules/db/src/blaze/db/tx_log/local.clj b/modules/db/src/blaze/db/tx_log/local.clj index a16f7cc85..336f94725 100644 --- a/modules/db/src/blaze/db/tx_log/local.clj +++ b/modules/db/src/blaze/db/tx_log/local.clj @@ -50,7 +50,7 @@ (into [] (take max-poll-size) tx-data)))) (defn- poll! [^BlockingQueue queue timeout] - (log/trace "poll in-memory queue with timeout =" timeout) + (log/trace "poll in-memory queue with timeout =" (str timeout)) (.poll queue (time/as timeout :millis) TimeUnit/MILLISECONDS)) (deftype LocalQueue [kv-store offset queue queue-start unsubscribe!] diff --git a/modules/fhir-structure/src/blaze/fhir/spec/spec.clj b/modules/fhir-structure/src/blaze/fhir/spec/spec.clj index 623a9d522..985e148e8 100644 --- a/modules/fhir-structure/src/blaze/fhir/spec/spec.clj +++ b/modules/fhir-structure/src/blaze/fhir/spec/spec.clj @@ -27,3 +27,18 @@ (s/def :blaze/resource #(s2/valid? :fhir/Resource %)) + +(s/def :fhir/CodeableConcept + #(s2/valid? :fhir/CodeableConcept %)) + +(s/def :fhir/Expression + #(s2/valid? :fhir/Expression %)) + +(s/def :fhir.Measure/group + #(s2/valid? :fhir.Measure/group %)) + +(s/def :fhir.Measure.group/stratifier + #(s2/valid? :fhir.Measure.group/stratifier %)) + +(s/def :fhir.Measure.group/population + #(s2/valid? :fhir.Measure.group/population %)) diff --git a/modules/interaction/src/blaze/interaction/transaction.clj b/modules/interaction/src/blaze/interaction/transaction.clj index 3ff91d57f..cbb93b2e9 100644 --- a/modules/interaction/src/blaze/interaction/transaction.clj +++ b/modules/interaction/src/blaze/interaction/transaction.clj @@ -14,6 +14,7 @@ [blaze.interaction.transaction.bundle :as bundle] [blaze.interaction.transaction.bundle.url :as url] [blaze.interaction.util :as iu] + [blaze.luid :as luid] [blaze.module :as m] [blaze.spec] [clojure.spec.alpha :as s] @@ -191,8 +192,8 @@ (defn- prepare-entry [res {{:keys [method]} :request :as entry}] (case (type/value method) "POST" - (let [entry (update entry :resource assoc :id (first (:luids res)))] - (-> (update res :luids next) + (let [entry (update entry :resource assoc :id (luid/head (::luid/generator res)))] + (-> (update res ::luid/generator luid/next) (update :entries conj entry))) (update res :entries conj entry))) @@ -225,7 +226,7 @@ (when-ok [entries (validate-entries entries)] (-> (reduce prepare-entry - {:luids (iu/successive-luids context) :entries []} + {::luid/generator (iu/luid-generator context) :entries []} entries) :entries)) entries)))) diff --git a/modules/interaction/src/blaze/interaction/util.clj b/modules/interaction/src/blaze/interaction/util.clj index c1c9f51b0..484dd3c2a 100644 --- a/modules/interaction/src/blaze/interaction/util.clj +++ b/modules/interaction/src/blaze/interaction/util.clj @@ -57,8 +57,8 @@ (defn luid [{:keys [clock rng-fn]}] (luid/luid clock (rng-fn))) -(defn successive-luids [{:keys [clock rng-fn]}] - (luid/successive-luids clock (rng-fn))) +(defn luid-generator [{:keys [clock rng-fn]}] + (luid/generator clock (rng-fn))) (defn- prep-if-none-match [if-none-match] (if (= "*" if-none-match) diff --git a/modules/luid/src/blaze/luid.clj b/modules/luid/src/blaze/luid.clj index 3626b2908..234ab97db 100644 --- a/modules/luid/src/blaze/luid.clj +++ b/modules/luid/src/blaze/luid.clj @@ -1,4 +1,5 @@ (ns blaze.luid + (:refer-clojure :exclude [next]) (:require [blaze.luid.impl :as impl]) (:import @@ -20,12 +21,27 @@ [clock rng] (impl/luid (.millis ^Clock clock) (entropy rng))) -(defn- successive-luids* [^long timestamp ^long entropy] - (cons (impl/luid timestamp entropy) - (lazy-seq - (if (= 0xFFFFFFFFF entropy) - (successive-luids* (inc timestamp) 0) - (successive-luids* timestamp (inc entropy)))))) +(defprotocol Generator + (-head [_]) + (-next [_])) -(defn successive-luids [clock rng] - (successive-luids* (.millis ^Clock clock) (entropy rng))) +(defn generator? [x] + (satisfies? Generator x)) + +(defn head [generator] + (-head generator)) + +(defn next [generator] + (-next generator)) + +(deftype LuidGenerator [^long timestamp ^long entropy] + Generator + (-head [_] + (impl/luid timestamp entropy)) + (-next [_] + (if (= 0xFFFFFFFFF entropy) + (LuidGenerator. (inc timestamp) 0) + (LuidGenerator. timestamp (inc entropy))))) + +(defn generator [clock rng] + (->LuidGenerator (.millis ^Clock clock) (entropy rng))) diff --git a/modules/luid/src/blaze/luid/spec.clj b/modules/luid/src/blaze/luid/spec.clj new file mode 100644 index 000000000..81e08e6a9 --- /dev/null +++ b/modules/luid/src/blaze/luid/spec.clj @@ -0,0 +1,7 @@ +(ns blaze.luid.spec + (:require + [blaze.luid :as luid] + [clojure.spec.alpha :as s])) + +(s/def ::luid/generator + luid/generator?) diff --git a/modules/luid/src/blaze/luid_spec.clj b/modules/luid/src/blaze/luid_spec.clj index 596cef47c..15f92fd16 100644 --- a/modules/luid/src/blaze/luid_spec.clj +++ b/modules/luid/src/blaze/luid_spec.clj @@ -1,6 +1,7 @@ (ns blaze.luid-spec (:require [blaze.luid :as luid] + [blaze.luid.spec] [blaze.spec] [clojure.spec.alpha :as s])) @@ -8,6 +9,18 @@ :args (s/cat :clock :blaze/clock :rng-fn :blaze/rng) :ret string?) -(s/fdef luid/successive-luids +(s/fdef luid/generator? + :args (s/cat :x any?) + :ret boolean?) + +(s/fdef luid/head + :args (s/cat :generator ::luid/generator) + :ret string?) + +(s/fdef luid/next + :args (s/cat :generator ::luid/generator) + :ret ::luid/generator) + +(s/fdef luid/generator :args (s/cat :clock :blaze/clock :rng-fn :blaze/rng) - :ret (s/every string?)) + :ret ::luid/generator) diff --git a/modules/luid/test/blaze/luid_test.clj b/modules/luid/test/blaze/luid_test.clj index b862dc48e..a34bf3ece 100644 --- a/modules/luid/test/blaze/luid_test.clj +++ b/modules/luid/test/blaze/luid_test.clj @@ -6,8 +6,7 @@ [clojure.math :as math] [clojure.spec.test.alpha :as st] [clojure.test :as test :refer [deftest is testing]] - [java-time.api :as time] - [juxt.iota :refer [given]]) + [java-time.api :as time]) (:import [java.time Clock Instant ZoneId] [java.util Random] @@ -48,15 +47,13 @@ (nextLong [] n))) -(deftest successive-luids-test - (testing "first 3 LUID's" - (given (take 3 (luid/successive-luids clock (fixed-random 0))) - 0 := (luid/luid clock (fixed-random 0)) - 1 := (luid/luid clock (fixed-random 1)) - 2 := (luid/luid clock (fixed-random 2)))) +(deftest generator-test + (testing "first 2 LUID's" + (let [gen (luid/generator clock (fixed-random 0))] + (is (= (luid/head gen) (luid/luid clock (fixed-random 0)))) + (is (= (luid/head (luid/next gen)) (luid/luid clock (fixed-random 1)))))) (testing "increments timestamp on entropy exhaustion" - (given (take 3 (luid/successive-luids clock (fixed-random 0xFFFFFFFFF))) - 0 := (luid/luid clock (fixed-random 0xFFFFFFFFF)) - 1 := (luid/luid (Clock/offset clock (time/millis 1)) (fixed-random 0)) - 2 := (luid/luid (Clock/offset clock (time/millis 1)) (fixed-random 1))))) + (let [gen (luid/generator clock (fixed-random 0xFFFFFFFFF))] + (is (= (luid/head gen) (luid/luid clock (fixed-random 0xFFFFFFFFF)))) + (is (= (luid/head (luid/next gen)) (luid/luid (Clock/offset clock (time/millis 1)) (fixed-random 0))))))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj index 81b8a75cc..a2f5b7ef9 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/cql.clj @@ -9,7 +9,6 @@ [blaze.elm.expression :as expr] [blaze.elm.util :as elm-util] [blaze.fhir.spec :as fhir-spec] - [blaze.util :refer [conj-vec]] [taoensso.timbre :as log]) (:import [java.time Duration])) @@ -60,11 +59,10 @@ (evaluate-expression-1* context subject name expression))) (defmulti result-xf - (fn [{:keys [return-handles? population-basis]} _ _] - [(if return-handles? :return-handles :count) - (if (nil? population-basis) :subject-based :population-based)])) + (fn [{:keys [population-basis]} _ _] + (if (nil? population-basis) :subject-based :population-based))) -(defmethod result-xf [:return-handles :subject-based] +(defmethod result-xf :subject-based [context name expression] (keep (fn [subject] @@ -73,40 +71,19 @@ (when matches? {:population-handle subject :subject-handle subject}))))) -(defmethod result-xf [:return-handles :population-based] +(defmethod result-xf :population-based [context name expression] (mapcat (fn [subject] - (when-ok [population-resources (evaluate-expression-1 context subject - name expression)] + (if-ok [population-resources (evaluate-expression-1 context subject + name expression)] (coll/eduction (map (fn [population-resource] {:population-handle population-resource :subject-handle subject})) - population-resources))))) - -(defmethod result-xf [:count :subject-based] - [context name expression] - (map - (fn [subject] - (when-ok [matches? (evaluate-expression-1 context subject name - expression)] - (if matches? 1 0))))) - -(defmethod result-xf [:count :population-based] - [context name expression] - (map - (fn [subject] - (when-ok [population-resources (evaluate-expression-1 context subject - name expression)] - (count population-resources))))) - -(defn- reduce-op [{:keys [return-handles?]}] - (if return-handles? conj +)) - -(defn- combine-op [{:keys [return-handles?]}] - (if return-handles? into +)) + population-resources) + vector)))) (defn- evaluate-expression*** [{:keys [db] :as context} {:keys [name expression]} subject-handles] @@ -115,7 +92,7 @@ (comp (map (partial ed/mk-resource db)) (result-xf (assoc context :db db) name expression) (halt-when ba/anomaly?)) - (reduce-op context) subject-handles))) + ((:reduce-op context) db) subject-handles))) (defn- evaluate-expression** "Returns a CompletableFuture that will complete with the result of evaluating @@ -140,15 +117,7 @@ (log/trace "Evaluate expression" (c/form (:expression expression-def))) (let [futures (evaluate-expression-futures context expression-def subject-type)] (do-sync [_ (ac/all-of futures)] - (transduce (map ac/join) (combine-op context) futures)))) - -(defn- missing-expression-anom [name] - (ba/incorrect - (format "Missing expression with name `%s`." name) - :expression-name name)) - -(defn- expression-def [{:keys [expression-defs]} name] - (or (get expression-defs name) (missing-expression-anom name))) + (transduce (map ac/join) (completing (:combine-op context)) futures)))) (defn- check-context "Returns an anomaly if `subject-type` differs from :context of @@ -192,33 +161,41 @@ :population-basis population-basis :expression-result-type result-type))))) +(defn- missing-expression-anom [name] + (ba/incorrect + (format "Missing expression with name `%s`." name) + :expression-name name)) + +(defn- expression-def [{:keys [expression-defs]} name] + (or (get expression-defs name) (missing-expression-anom name))) + (defn evaluate-expression "Evaluates the expression with `name` on each subject of `subject-type` available in :db of `context`. The context consists of: - * context :db - the database to use for obtaining subjects and - evaluating the expression - * context :executor - the executor in which the expression is evaluated - * context :now - the evaluation time - * context :timeout-eclipsed? - a function returning ture if the evaluation - timeout is eclipsed - * context :timeout - the evaluation timeout itself - * context :expression-defs - a map of available expression definitions - * context :parameters - an optional map of parameters - * context :return-handles? - whether subject-handles or a simple count - should be returned - * context :population-basis - an optional population basis of a type like - `Encounter` - * name - the name of the expression - * subject-type - the type of subjects like `Patient` + * :db - the database to use for obtaining subjects and + evaluating the expression + * :executor - the executor in which the expression is evaluated + * :now - the evaluation time + * :timeout-eclipsed? - a function returning ture if the evaluation timeout is + eclipsed + * :timeout - the evaluation timeout itself + * :expression-defs - a map of available expression definitions + * :parameters - an optional map of parameters + * :reduce-op - a reduce function that gets the result reduced so far + and a map of :population-handle and :subject-handle. + The function also has to return an initial value if + called with no argument + * :combine-op - a combine function that gets two already reduced + results and returns a new one + * :population-basis - an optional population basis of a type like `Encounter` The context of the expression has to match `subject-type`. The result type of the expression has to match the `population-basis`. - Returns a CompletableFuture that will complete with a list of subject-handles - or a simple count or will complete exceptionally with an anomaly in\n case of - errors." + Returns a CompletableFuture that will complete with the result of :combine-op + or will complete exceptionally with an anomaly in case of errors." [{:keys [population-basis] :as context} name subject-type] (if-ok [expression-def (expression-def context name) _ (check-context subject-type expression-def) @@ -230,111 +207,41 @@ "Evaluates the expression with `name` on `subject` according to `context`. The context consists of: - * context :db - the database to use for obtaining subjects and - evaluating the expression - * context :executor - the executor in which the expression is evaluated - * context :now - the evaluation time - * context :timeout-eclipsed? - a function returning ture if the evaluation - timeout is eclipsed - * context :timeout - the evaluation timeout itself - * context :expression-defs - a map of available expression definitions - * context :parameters - an optional map of parameters - * context :return-handles? - whether subject-handles or a simple count - should be returned + * :db - the database to use for obtaining subjects and + evaluating the expression + * :executor - the executor in which the expression is evaluated + * :now - the evaluation time + * :timeout-eclipsed? - a function returning ture if the evaluation timeout is + eclipsed + * :timeout - the evaluation timeout itself + * :expression-defs - a map of available expression definitions + * :parameters - an optional map of parameters + * :reduce-op - a reduce function that gets the result reduced so far + and a map of :population-handle and :subject-handle. + The function also has to return an initial value if + called with no argument + * :combine-op - a combine function that gets two already reduced + results and returns a new one Returns an anomaly in case of errors." - [{:keys [executor return-handles?] :as context} subject name] + [{:keys [executor reduce-op db] :as context} subject name] (if-ok [{:keys [name expression]} (expression-def context name)] (ac/supply-async #(when-ok [matches? (evaluate-expression-1 context subject name expression)] - (if matches? - (if return-handles? - [{:population-handle subject - :subject-handle subject}] - 1) - (if return-handles? [] 0))) + (let [reduce-op (reduce-op db)] + (cond-> (reduce-op) + matches? + (reduce-op {:population-handle subject + :subject-handle subject})))) executor) ac/completed-future)) -(defn- incorrect-stratum-msg [{:keys [id] :as resource} expression-name] - (format "CQL expression `%s` returned more than one value for resource `%s/%s`." - expression-name (-> resource fhir-spec/fhir-type name) id)) - -(defn- evaluate-stratum-expression [context subject name expression] - (let [result (evaluate-expression-1 context subject name expression)] - (if (sequential? result) - (ba/incorrect (incorrect-stratum-msg subject name)) - result))) - -(defn- calc-strata*** - [{:keys [db] :as context} {:keys [name expression]} handles] - (with-open [db (d/new-batch-db db)] - (let [context (assoc context :db db)] - (reduce - (fn [ret {:keys [subject-handle] :as handle}] - (if-ok [stratum (evaluate-stratum-expression context subject-handle - name expression)] - (update ret stratum conj-vec handle) - reduced)) - {} handles)))) - -(defn- calc-strata** [{:keys [executor] :as context} expression-def handles] - (ac/supply-async - #(calc-strata*** context expression-def handles) - executor)) - -(defn- calc-strata-futures [context expression-def handles] +(defn evaluate-multi-component-stratum [context handle evaluators] (into [] - (comp - (partition-all eval-sequential-chunk-size) - (map (partial calc-strata** context expression-def))) - handles)) - -(defn- calc-strata* [context expression-def handles] - (let [futures (calc-strata-futures context expression-def handles)] - (do-sync [_ (ac/all-of futures)] - (transduce (map ac/join) (partial merge-with into) futures)))) - -(defn calc-strata - "Returns a CompletableFuture that will complete with a map of stratum value to - a list of subject handles or will complete exceptionally with an anomaly in - case of errors." - [context expression-name handles] - (if-ok [expression-def (expression-def context expression-name)] - (calc-strata* context expression-def handles) - ac/completed-future)) - -(defn- calc-function-strata*** - [{:keys [db] :as context} {:keys [name function]} handles] - (with-open [db (d/new-batch-db db)] - (let [context (assoc context :db db)] - (reduce - (fn [ret {:keys [population-handle subject-handle] :as handle}] - (if-ok [stratum (evaluate-stratum-expression context subject-handle - name (function [population-handle]))] - (update ret stratum conj-vec handle) - reduced)) - {} handles)))) - -(defn- calc-function-strata** - [{:keys [executor] :as context} function-def handles] - (ac/supply-async - #(calc-function-strata*** context function-def handles) - executor)) - -(defn- calc-function-strata-futures [context function-def handles] - (into - [] - (comp - (partition-all eval-sequential-chunk-size) - (map (partial calc-function-strata** context function-def))) - handles)) - -(defn- calc-function-strata* [context function-def handles] - (let [futures (calc-function-strata-futures context function-def handles)] - (do-sync [_ (ac/all-of futures)] - (transduce (map ac/join) (partial merge-with into) futures)))) + (comp (map #(% context handle)) + (halt-when ba/anomaly?)) + evaluators)) (defn- missing-function-anom [name] (ba/incorrect @@ -344,77 +251,37 @@ (defn- function-def [{:keys [function-defs]} name] (or (get function-defs name) (missing-function-anom name))) -(defn calc-function-strata - "Returns a CompletableFuture that will complete with a map of stratum value to - a list of subject handles or will complete exceptionally with an anomaly in - case of errors." - [context function-name handles] - (if-ok [function-def (function-def context function-name)] - (calc-function-strata* context function-def handles) - ac/completed-future)) +(defn- expression-or-function-def + [{:keys [expression-defs population-basis] :as context} name] + (or (get expression-defs name) + (if (string? population-basis) + (function-def context name) + (missing-expression-anom name)))) -(defn- evaluate-multi-component-stratum-1 - [context - {:keys [subject-handle population-handle]} - {:keys [name expression function]}] - (if function - (evaluate-stratum-expression context subject-handle name (function [population-handle])) - (evaluate-stratum-expression context subject-handle name expression))) +(defn- incorrect-stratum-msg [{:keys [id] :as resource} expression-name] + (format "CQL expression `%s` returned more than one value for resource `%s/%s`." + expression-name (-> resource fhir-spec/fhir-type name) id)) -(defn- evaluate-multi-component-stratum [context handle defs] - (transduce - (comp (map (partial evaluate-multi-component-stratum-1 context handle)) - (halt-when ba/anomaly?)) - conj - defs)) +(defn- evaluate-stratum-expression [context subject name expression] + (let [result (evaluate-expression-1 context subject name expression)] + (if (sequential? result) + (ba/incorrect (incorrect-stratum-msg subject name)) + result))) -(defn calc-multi-component-strata*** [{:keys [db] :as context} defs handles] - (with-open [db (d/new-batch-db db)] - (let [context (assoc context :db db)] - (reduce - (fn [ret handle] - (if-ok [stratum (evaluate-multi-component-stratum context handle defs)] - (update ret stratum conj-vec handle) - reduced)) - {} handles)))) - -(defn- calc-multi-component-strata** - [{:keys [executor] :as context} defs handles] - (ac/supply-async - #(calc-multi-component-strata*** context defs handles) - executor)) +(defn stratum-expression-evaluator* [{:keys [name expression function]}] + (if function + (fn [context {:keys [subject-handle population-handle]}] + (evaluate-stratum-expression context subject-handle name (function [population-handle]))) + (fn [context {:keys [subject-handle]}] + (evaluate-stratum-expression context subject-handle name expression)))) -(defn- calc-multi-component-strata-futures [context defs handles] +(defn stratum-expression-evaluator [context name] + (when-ok [def (expression-or-function-def context name)] + (stratum-expression-evaluator* def))) + +(defn stratum-expression-evaluators [context names] (into [] - (comp - (partition-all eval-sequential-chunk-size) - (map (partial calc-multi-component-strata** context defs))) - handles)) - -(defn calc-multi-component-strata* [context defs handles] - (let [futures (calc-multi-component-strata-futures context defs handles)] - (do-sync [_ (ac/all-of futures)] - (transduce (map ac/join) (partial merge-with into) futures)))) - -(defn- def [{:keys [expression-defs population-basis] :as context} name] - (or (get expression-defs name) - (if (string? population-basis) - (function-def context name) - (missing-expression-anom name)))) - -(defn- defs [context names] - (transduce - (comp (map (partial def context)) + (comp (map (partial stratum-expression-evaluator context)) (halt-when ba/anomaly?)) - conj names)) - -(defn calc-multi-component-strata - "Returns a CompletableFuture that will complete with a map of stratum value to - a list of subject handles or will complete exceptionally with an anomaly in - case of errors." - [context expression-names handles] - (if-ok [defs (defs context expression-names)] - (calc-multi-component-strata* context defs handles) - ac/completed-future)) 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 e82fde944..8b75dffe7 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 @@ -1,7 +1,7 @@ (ns blaze.fhir.operation.evaluate-measure.measure (:require - [blaze.anomaly :as ba :refer [when-ok]] - [blaze.async.comp :as ac :refer [do-sync do-async]] + [blaze.anomaly :as ba :refer [if-ok when-ok]] + [blaze.async.comp :as ac :refer [do-async do-sync]] [blaze.coll.core :as coll] [blaze.cql-translator :as cql-translator] [blaze.db.api :as d] @@ -9,8 +9,9 @@ [blaze.elm.compiler.external-data :as ed] [blaze.elm.compiler.library :as library] [blaze.fhir.operation.evaluate-measure.cql :as cql] - [blaze.fhir.operation.evaluate-measure.measure.population :as population] - [blaze.fhir.operation.evaluate-measure.measure.stratifier :as stratifier] + [blaze.fhir.operation.evaluate-measure.measure.group :as group] + [blaze.fhir.operation.evaluate-measure.measure.population :as pop] + [blaze.fhir.operation.evaluate-measure.measure.stratifier :as strat] [blaze.fhir.operation.evaluate-measure.measure.util :as u] [blaze.fhir.spec :as fhir-spec] [blaze.fhir.spec.type :as type] @@ -148,28 +149,39 @@ ;; ---- Evaluation ------------------------------------------------------------ (defn- evaluate-population-futures [context populations] - (into [] (map-indexed (partial population/evaluate context)) populations)) + (into [] (map-indexed (partial pop/evaluate context)) populations)) -(defn- evaluate-populations [context populations] - (let [futures (evaluate-population-futures context populations)] +(defn- evaluate-group* + [{:group/keys [combine-op] :as context} {:keys [population]}] + (let [futures (evaluate-population-futures context population)] (do-sync [_ (ac/all-of futures)] - (transduce (map ac/join) (population/reduce-op context) futures)))) - -(defn- evaluate-stratifiers - [{:keys [luids] :as context} evaluated-populations stratifiers] - (transduce - (map-indexed vector) - (completing - (fn [ret [idx stratifier]] - (ac/then-compose - ret - (fn [{:keys [luids] :as ret}] - (-> (stratifier/evaluate - (assoc context :luids luids :stratifier-idx idx) - evaluated-populations stratifier) - (ac/then-apply (partial u/merge-result ret))))))) - (ac/completed-future {:result [] :luids luids :tx-ops []}) - stratifiers)) + (transduce (map ac/join) combine-op futures)))) + +(defn- stratifiers-reduce-op [context stratifiers] + (when-ok [reduce-ops + (into + [] + (comp (map-indexed + (fn [idx stratifier] + (strat/reduce-op + (assoc context :stratifier-idx idx) + stratifier))) + (halt-when ba/anomaly?)) + stratifiers)] + (fn [db] + (let [reduce-ops (mapv #(% db) reduce-ops)] + (fn + ([] (mapv #(%) reduce-ops)) + ([rets x] + (loop [[reduce-op & reduce-ops] reduce-ops + [ret & rets] rets + result []] + (if reduce-op + (let [ret (reduce-op ret x)] + (if (reduced? ret) + ret + (recur reduce-ops rets (conj result ret)))) + result)))))))) (defn- population-basis [{:keys [extension]}] (some @@ -180,48 +192,91 @@ basis)))) extension)) -(defn- evaluate-group - {:arglists '([context group])} - [{:keys [report-type] :as context} - {:keys [code population stratifier] :as group}] - (let [context (assoc context - :population-basis (population-basis group) - :return-handles? (or (= "subject-list" report-type) - (some? (seq stratifier))))] - (-> (evaluate-populations context population) - (ac/then-compose - (fn [{:keys [luids] :as evaluated-populations}] - (-> (evaluate-stratifiers (assoc context :luids luids) - evaluated-populations - stratifier) - (ac/then-apply - (fn [{:keys [luids] :as evaluated-stratifiers}] - {:result - (cond-> {:fhir/type :fhir.MeasureReport/group} - code - (assoc :code code) - - (seq (:result evaluated-populations)) - (assoc :population (:result evaluated-populations)) - - (seq (:result evaluated-stratifiers)) - (assoc :stratifier (:result evaluated-stratifiers))) - :luids luids - :tx-ops - (into (:tx-ops evaluated-populations) - (:tx-ops evaluated-stratifiers))})))))))) - -(defn- evaluate-groups* [{:keys [luids] :as context} groups] +(defn- assoc-ops + [{:keys [report-type] luid-generator ::luid/generator :as context} + {:keys [code stratifier]}] + (if (= "subject-list" report-type) + (if (seq stratifier) + (when-ok [reduce-op (stratifiers-reduce-op context stratifier)] + (assoc + context + :reduce-op + (fn [db] + (let [reduce-op (reduce-op db)] + (fn + ([] {:handles [] :strata (reduce-op)}) + ([ret] ret) + ([{:keys [handles strata]} handle] + (let [strata (reduce-op strata handle)] + (if (reduced? strata) + strata + {:handles (conj handles handle) + :strata strata})))))) + :combine-op + (fn + ([] + {:handles [] + :strata (mapv (constantly {}) stratifier)}) + ([{handles-a :handles strata-a :strata} + {handles-b :handles strata-b :strata}] + {:handles (into handles-a handles-b) + :strata (mapv (partial merge-with into) strata-a strata-b)})) + :group/combine-op + (group/combine-op-subject-list-stratifier luid-generator code stratifier))) + (assoc + context + :reduce-op (fn [_db] conj) + :combine-op into + :group/combine-op (group/combine-op-subject-list luid-generator code))) + (if (seq stratifier) + (when-ok [reduce-op (stratifiers-reduce-op context stratifier)] + (assoc + context + :reduce-op + (fn [db] + (let [reduce-op (reduce-op db)] + (fn + ([] {:count 0 :strata (reduce-op)}) + ([ret] ret) + ([{:keys [count strata]} handles] + (let [strata (reduce-op strata handles)] + (if (reduced? strata) + strata + {:count (inc count) + :strata strata})))))) + :combine-op + (fn + ([] + {:count 0 + :strata (mapv (constantly {}) stratifier)}) + ([{count-a :count strata-a :strata} {count-b :count strata-b :strata}] + {:count (+ count-a count-b) + :strata (mapv (partial merge-with +) strata-a strata-b)})) + :group/combine-op + (group/combine-op-count-stratifier luid-generator code stratifier))) + (assoc + context + :reduce-op (fn [_db] ((map (constantly 1)) +)) + :combine-op + + :group/combine-op (group/combine-op-count luid-generator code))))) + +(defn- evaluate-group [context group] + (if-ok [context (-> (assoc context :population-basis (population-basis group)) + (assoc-ops group))] + (evaluate-group* context group) + ac/completed-future)) + +(defn- evaluate-groups* [{::luid/keys [generator] :as context} groups] (transduce (map-indexed vector) (completing (fn [ret [idx group]] (ac/then-compose ret - (fn [{:keys [luids] :as ret}] - (-> (evaluate-group (assoc context :luids luids :group-idx idx) group) + (fn [{::luid/keys [generator] :as ret}] + (-> (evaluate-group (assoc context ::luid/generator generator :group-idx idx) group) (ac/then-apply (partial u/merge-result ret))))))) - (ac/completed-future {:result [] :luids luids :tx-ops []}) + (ac/completed-future {:result [] ::luid/generator generator :tx-ops []}) groups)) (defn- evaluate-groups-msg [id subject-type duration] @@ -292,8 +347,8 @@ (defn- now [clock] (OffsetDateTime/now ^Clock clock)) -(defn- successive-luids [{:keys [clock rng-fn]}] - (luid/successive-luids clock (rng-fn))) +(defn- luid-generator [{:keys [clock rng-fn]}] + (luid/generator clock (rng-fn))) (defn- missing-subject-msg [type id] (format "Subject with type `%s` and id `%s` was not found." type id)) @@ -341,24 +396,27 @@ (compile-primary-library db measure {})] (when-ok [subject-handle (some->> subject-ref (subject-handle db subject-type))] (let [context - (assoc context - :db db - :now now - :timeout-eclipsed? #(not (.isBefore (.instant ^Clock clock) timeout-instant)) - :timeout timeout - :expression-defs expression-defs - :function-defs function-defs - :parameters parameter-default-values - :subject-type subject-type - :report-type report-type - :luids (successive-luids context))] + (cond-> + (assoc + context + :db db + :now now + :timeout-eclipsed? #(not (.isBefore (.instant ^Clock clock) timeout-instant)) + :timeout timeout + :expression-defs expression-defs + :parameters parameter-default-values + :subject-type subject-type + :report-type report-type + ::luid/generator (luid-generator context)) + function-defs + (assoc :function-defs function-defs))] (when-ok [expression-defs (eval-unfiltered context expression-defs)] (cond-> (assoc context :expression-defs expression-defs) subject-handle (assoc :subject-handle (ed/mk-resource db subject-handle))))))))) (defn evaluate-measure - "Evaluates `measure` inside `period` in `db` with evaluation time of `now`. + "Evaluates `measure` inside `context` with `params`. Returns a CompletableFuture that will complete with an already completed MeasureReport under :resource which isn't persisted and optional :tx-ops or diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/group.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/group.clj new file mode 100644 index 000000000..0f36f58ff --- /dev/null +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/group.clj @@ -0,0 +1,164 @@ +(ns blaze.fhir.operation.evaluate-measure.measure.group + (:require + [blaze.fhir.operation.evaluate-measure.measure.population :as pop] + [blaze.fhir.operation.evaluate-measure.measure.stratifier :as strat] + [blaze.fhir.operation.evaluate-measure.measure.util :as u] + [blaze.luid :as luid])) + +(defn- report-stratifier [{:keys [code component]}] + (cond-> {:fhir/type :fhir.MeasureReport.group/stratifier} + code + (assoc :code [code]) + (seq component) + (assoc :component-codes (:codes (strat/extract-stratifier-components nil component))))) + +(defn- initial-group + ([code] + (cond-> {:fhir/type :fhir.MeasureReport/group + :population []} + code + (assoc :code code))) + ([code stratifiers] + (assoc (initial-group code) :stratifier (mapv report-stratifier stratifiers)))) + +(defn- initial-state + ([luid-generator code] + {:result (initial-group code) + ::luid/generator luid-generator + :tx-ops []}) + ([luid-generator code stratifiers] + {:result (initial-group code stratifiers) + ::luid/generator luid-generator + :tx-ops []})) + +(defn combine-op-count + "Returns an combine operator that combines the count results of the + populations within the group with `code`." + [luid-generator code] + (fn + ([] (initial-state luid-generator code)) + ([state] state) + ([state [code count]] + (update-in state [:result :population] conj (pop/population code count))))) + +(defn- post-process-strata [strata] + (mapv strat/stratum-count strata)) + +(defn- post-process-multi-component-strata [strata component-codes] + (mapv (partial strat/multi-component-stratum-count component-codes) strata)) + +(defn- post-process-count-stratifier [{:keys [component-codes] :as stratifier}] + (if (seq component-codes) + (-> (update stratifier :stratum post-process-multi-component-strata component-codes) + (dissoc :component-codes)) + (update stratifier :stratum post-process-strata))) + +(defn- post-process-count-stratifiers [stratifiers] + (mapv post-process-count-stratifier stratifiers)) + +(defn combine-op-count-stratifier + "Returns an combine operator that combines the count results of the + populations within the group with `code` that also has stratifiers." + [luid-generator code stratifiers] + (fn + ([] (initial-state luid-generator code stratifiers)) + ([state] + (update-in state [:result :stratifier] post-process-count-stratifiers)) + ([state [code {:keys [count strata]}]] + (update + state + :result + (fn [result] + (-> (update result :population conj (pop/population code count)) + (update + :stratifier + (fn [stratifier] + (mapv + (fn [stratifier strata] + (update + stratifier + :stratum + #(reduce-kv + (fn [stratum value count] + (update-in stratum [value code] (fnil + 0) count)) + % + strata))) + stratifier strata))))))))) + +(defn- subject-list-population [code handles list-id] + (assoc (pop/population code (count handles)) :subjectResults (u/list-reference list-id))) + +(defn- combine-handles [{::luid/keys [generator] :keys [result tx-ops]} code handles] + (let [list-id (luid/head generator)] + {:result (update result :population conj (subject-list-population code handles list-id)) + ::luid/generator (luid/next generator) + :tx-ops (into tx-ops (u/population-tx-ops list-id handles))})) + +(defn combine-op-subject-list + "Returns an combine operator that combines the subject-list results of the + populations within the group with `code`." + [luid-generator code] + (fn + ([] (initial-state luid-generator code)) + ([state] state) + ([state [code handles]] + (combine-handles state code handles)))) + +(defn- append [rf] + (fn + ([r] (rf r)) + ([{:keys [result] :as ret} x] + (update (rf ret x) :result (partial conj result))))) + +(defn- post-process-subject-list-stratifier + [context {:keys [component-codes] :as stratifier}] + (let [stratum-fn + (if (seq component-codes) + (partial strat/multi-component-stratum-subject-list component-codes) + strat/stratum-subject-list) + stratifier (dissoc stratifier :component-codes)] + (transduce + append + (fn + ([ret] + (update ret :result (partial assoc stratifier :stratum))) + ([ret [value populations]] + (stratum-fn ret value populations))) + (assoc context :result []) + (:stratum stratifier)))) + +(defn- post-process-subject-list-stratifier-group [context group] + (transduce + append + (fn + ([ret] + (update ret :result (partial assoc group :stratifier))) + ([ret stratifier] + (post-process-subject-list-stratifier ret stratifier))) + (assoc context :result []) + (:stratifier group))) + +(defn combine-op-subject-list-stratifier + "Returns an combine operator that combines the subject-list results of the + populations within the group with `code` that also has stratifiers." + [luid-generator code stratifiers] + (fn + ([] (initial-state luid-generator code stratifiers)) + ([{group :result :as state}] + (post-process-subject-list-stratifier-group state group)) + ([state [code {:keys [handles strata]}]] + (update-in + (combine-handles state code handles) + [:result :stratifier] + (fn [stratifier] + (mapv + (fn [stratifier strata] + (update + stratifier + :stratum + #(reduce-kv + (fn [stratum value handles] + (update-in stratum [value code] (fnil into []) handles)) + % + strata))) + stratifier strata)))))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/population.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/population.clj index 06878b8ed..fff07d577 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/population.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/population.clj @@ -3,7 +3,8 @@ [blaze.anomaly :refer [if-ok]] [blaze.async.comp :as ac :refer [do-sync]] [blaze.fhir.operation.evaluate-measure.cql :as cql] - [blaze.fhir.operation.evaluate-measure.measure.util :as u])) + [blaze.fhir.operation.evaluate-measure.measure.util :as u] + [blaze.fhir.spec.type :as type])) (defn- population-path [group-idx population-idx] (format "Measure.group[%d].population[%d]" group-idx population-idx)) @@ -16,37 +17,18 @@ (cql/evaluate-expression context expression-name subject-type))) (defn evaluate - "Returns a CompletableFuture that will complete with a map of :result, - :handles, :luids and :tx-ops or will complete exceptionally with an anomaly in - case of errors." + "Returns a CompletableFuture that will complete with a tuple of population + code and result or will complete exceptionally with an anomaly in case of + errors." {:arglists '([context idx population])} [{:keys [group-idx] :as context} idx {:keys [code criteria]}] (let [population-path-fn #(population-path group-idx idx)] - (if-ok [expression-name (u/expression population-path-fn criteria)] + (if-ok [expression-name (u/expression-name population-path-fn criteria)] (do-sync [result (evaluate-expression context expression-name)] [code result]) ac/completed-future))) -(defn reduce-op - {:arglists '([context])} - [{:keys [return-handles? luids] :as context}] - (if return-handles? - (fn - ([] - {:result [] :handles [] :luids luids :tx-ops []}) - ([ret] - ret) - ([{:keys [luids] :as ret} [code result]] - (->> (-> (u/population (assoc context :luids luids) - :fhir.MeasureReport.group/population code result) - (assoc :handles result)) - (u/merge-result ret)))) - (fn - ([] - {:result [] :handles [] :luids luids :tx-ops []}) - ([ret] - ret) - ([{:keys [luids] :as ret} [code result]] - (->> (u/population-count (assoc context :luids luids) - :fhir.MeasureReport.group/population code result) - (u/merge-result ret)))))) +(defn population [code count] + {:fhir/type :fhir.MeasureReport.group/population + :count (type/integer count) + :code code}) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/stratifier.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/stratifier.clj index 54c55364d..bf04b6e0f 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/stratifier.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/stratifier.clj @@ -1,12 +1,16 @@ (ns blaze.fhir.operation.evaluate-measure.measure.stratifier (:require [blaze.anomaly :as ba :refer [if-ok when-ok]] - [blaze.async.comp :as ac :refer [do-sync]] [blaze.fhir.operation.evaluate-measure.cql :as cql] [blaze.fhir.operation.evaluate-measure.measure.util :as u] - [blaze.fhir.spec.type :as type])) - -(defn- value-concept [value] + [blaze.fhir.spec.type :as type] + [blaze.luid :as luid] + [blaze.util :refer [conj-vec]])) + +(defn- value-concept + "Converts `value` into a CodeableConcept so that it can be used in a + Stratifier." + [value] (let [type (type/type value)] (cond (identical? :fhir/CodeableConcept type) @@ -25,51 +29,82 @@ {:url "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.value" :value value})) -(defn- stratum* [population value] +(defn stratum-count [[value populations]] (cond-> {:fhir/type :fhir.MeasureReport.group.stratifier/stratum :value (value-concept value) - :population [population]} + :population + (mapv + (fn [[code count]] + {:fhir/type :fhir.MeasureReport.group.stratifier.stratum/population + :code code + :count (type/integer count)}) + populations)} (identical? :fhir/Quantity (type/type value)) (assoc :extension [(stratum-value-extension value)]))) -(defn- stratum [context population-code value handles] - (-> (u/population - context :fhir.MeasureReport.group.stratifier.stratum/population - population-code handles) - (update :result stratum* value))) - -(defn- stratifier* [strata code] - (cond-> {:fhir/type :fhir.MeasureReport.group/stratifier - :stratum (vec (sort-by (comp type/value :text :value) strata))} - code - (assoc :code [code]))) - -(defn- stratifier [{:keys [luids] :as context} code population-code strata] - (-> (reduce-kv - (fn [{:keys [luids] :as ret} value handles] - (->> (stratum (assoc context :luids luids) population-code value handles) - (u/merge-result ret))) - {:result [] :luids luids :tx-ops []} - strata) - (update :result stratifier* code))) +(defn- stratum-subject-list-populations [context populations] + (reduce + (fn [{::luid/keys [generator] :keys [result tx-ops]} [code handles]] + (let [list-id (luid/head generator)] + {:result + (conj + result + (cond-> + {:fhir/type :fhir.MeasureReport.group.stratifier.stratum/population + :count (type/integer (count handles)) + :subjectResults (u/list-reference list-id)} + code + (assoc :code code))) + ::luid/generator (luid/next generator) + :tx-ops (into tx-ops (u/population-tx-ops list-id handles))})) + (assoc context :result []) + populations)) + +(defn stratum-subject-list + "Creates a stratum from `value` and `populations` in case subject lists are + requested. + + Uses ::luid/generator from `context` as generator for list ids and :tx-ops + from `context` to append the transaction operators for creating the subject + lists. + + Returns a map of the stratum as :result, the new ::luid/generator and the + appended :tx-ops." + [context value populations] + (update + (stratum-subject-list-populations context populations) + :result + #(cond-> {:fhir/type :fhir.MeasureReport.group.stratifier/stratum + :value (value-concept value) + :population %} + + (identical? :fhir/Quantity (type/type value)) + (assoc :extension [(stratum-value-extension value)])))) (defn- stratifier-path [group-idx stratifier-idx] (format "Measure.group[%d].stratifier[%d]" group-idx stratifier-idx)) -(defn- calc-strata [{:keys [population-basis] :as context} name handles] - (if (nil? population-basis) - (cql/calc-strata context name handles) - (cql/calc-function-strata context name handles))) - -(defn- evaluate-stratifier - {:arglists '([context evaluated-populations stratifier])} - [{:keys [group-idx stratifier-idx] :as context} evaluated-populations - {:keys [code criteria]}] - (if-ok [name (u/expression #(stratifier-path group-idx stratifier-idx) criteria)] - (do-sync [strata (calc-strata context name (-> evaluated-populations :handles first))] - (stratifier context code (-> evaluated-populations :result first :code) strata)) - ac/completed-future)) +(defn- reduce-op** [{:keys [report-type] :as context} name] + (let [stratum-update + (if (= "subject-list" report-type) + conj-vec + (fn [count _handle] (inc (or count 0))))] + (when-ok [evaluator (cql/stratum-expression-evaluator context name)] + (fn [db] + (let [context (assoc context :db db)] + (fn + ([] {}) + ([ret handle] + (if-ok [stratum (evaluator context handle)] + (update ret stratum stratum-update handle) + reduced)))))))) + +(defn- reduce-op* + [{:keys [group-idx stratifier-idx] :as context} {:keys [criteria]}] + (let [path-fn #(stratifier-path group-idx stratifier-idx)] + (when-ok [expression-name (u/expression-name path-fn criteria)] + (reduce-op** context expression-name)))) (defn- stratifier-component-path [{:keys [group-idx stratifier-idx component-idx]}] (format "Measure.group[%d].stratifier[%d].component[%d]" @@ -83,9 +118,9 @@ "Missing code." :fhir/issue "required" :fhir.issue/expression (str (stratifier-component-path context) ".code")) - (when-ok [expression (u/expression #(stratifier-component-path context) - criteria)] - [code expression]))) + (let [path-fn #(stratifier-component-path context)] + (when-ok [expression-name (u/expression-name path-fn criteria)] + [code expression-name])))) (defn- extract-stratifier-component* ([_context x] x) @@ -97,7 +132,7 @@ (update :expression-names conj expression-name)) reduced))) -(defn- extract-stratifier-components +(defn extract-stratifier-components "Extracts code and expression-name from each of `stratifier-components`." [context stratifier-components] (transduce @@ -112,58 +147,77 @@ {:url "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.component.value" :value value})) -(defn- multi-component-stratum* [population codes values] +(defn components [codes values] + (mapv + (fn [code value] + (cond-> {:fhir/type :fhir.MeasureReport.group.stratifier.stratum/component + :code code + :value (value-concept value)} + + (identical? :fhir/Quantity (type/type value)) + (assoc :extension [(stratum-component-value-extension value)]))) + codes + values)) + +(defn multi-component-stratum-count [codes [values populations]] {:fhir/type :fhir.MeasureReport.group.stratifier/stratum - :component + :component (components codes values) + :population (mapv - (fn [code value] - (cond-> {:fhir/type :fhir.MeasureReport.group.stratifier.stratum/component - :code code - :value (value-concept value)} - - (identical? :fhir/Quantity (type/type value)) - (assoc :extension [(stratum-component-value-extension value)]))) - codes - values) - :population [population]}) - -(defn- multi-component-stratum [context codes population-code values result] - (-> (u/population - context :fhir.MeasureReport.group.stratifier.stratum/population - population-code result) - (update :result multi-component-stratum* codes values))) - -(defn- multi-component-stratifier* [strata codes] - {:fhir/type :fhir.MeasureReport.group/stratifier - :code codes - :stratum (vec (sort-by (comp #(mapv (comp type/value :text :value) %) :component) strata))}) - -(defn- multi-component-stratifier - [{:keys [luids] :as context} codes population-code strata] - (-> (reduce-kv - (fn [{:keys [luids] :as ret} values result] - (->> (multi-component-stratum (assoc context :luids luids) codes - population-code values result) - (u/merge-result ret))) - {:result [] :luids luids :tx-ops []} - strata) - (update :result multi-component-stratifier* codes))) - -(defn- evaluate-multi-component-stratifier - [context evaluated-populations {:keys [component]}] - (if-ok [{:keys [codes expression-names]} (extract-stratifier-components context component)] - (do-sync [strata (cql/calc-multi-component-strata - context - expression-names - (-> evaluated-populations :handles first))] - (multi-component-stratifier context codes - (-> evaluated-populations :result first :code) - strata)) - ac/completed-future)) - -(defn evaluate - {:arglists '([context evaluated-populations stratifier])} - [context evaluated-populations {:keys [component] :as stratifier}] + (fn [[code count]] + {:fhir/type :fhir.MeasureReport.group.stratifier.stratum/population + :code code + :count (type/integer count)}) + populations)}) + +(defn multi-component-stratum-subject-list + "Creates a stratum with multiple components with `codes` from `values` and + `populations` in case subject lists are requested. + + Uses ::luid/generator from `context` as generator for list ids and :tx-ops + from `context` to append the transaction operators for creating the subject + lists. + + Returns a map of the stratum as :result, the new ::luid/generator and the + appended :tx-ops." + [codes context values populations] + (update + (stratum-subject-list-populations context populations) + :result + #(cond-> {:fhir/type :fhir.MeasureReport.group.stratifier/stratum + :component (components codes values) + :population %} + + (identical? :fhir/Quantity (type/type values)) + (assoc :extension [(stratum-value-extension values)])))) + +(defn- multi-component-reduce-op* [{:keys [report-type] :as context} expression-names] + (let [stratum-update + (if (= "subject-list" report-type) + conj-vec + (fn [count _handle] (inc (or count 0))))] + (when-ok [evaluators (cql/stratum-expression-evaluators context expression-names)] + (fn [db] + (let [context (assoc context :db db)] + (fn + ([] {}) + ([ret handle] + (if-ok [stratum (cql/evaluate-multi-component-stratum context handle evaluators)] + (update ret stratum stratum-update handle) + reduced)))))))) + +(defn- multi-component-reduce-op + [context {:keys [component]}] + (when-ok [{:keys [expression-names]} (extract-stratifier-components context component)] + (multi-component-reduce-op* context expression-names))) + +(defn reduce-op + "Returns a function that when called with a database will return a reduce + operator or an anomaly in case of errors. + + The reduce operator will accumulate strata over handles." + {:arglists '([context stratifier])} + [context {:keys [component] :as stratifier}] (if (seq component) - (evaluate-multi-component-stratifier context evaluated-populations stratifier) - (evaluate-stratifier context evaluated-populations stratifier))) + (multi-component-reduce-op context stratifier) + (reduce-op* context stratifier))) diff --git a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj index e95ebe432..1c4b98de9 100644 --- a/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj +++ b/modules/operation-measure-evaluate-measure/src/blaze/fhir/operation/evaluate_measure/measure/util.clj @@ -1,9 +1,10 @@ (ns blaze.fhir.operation.evaluate-measure.measure.util (:require [blaze.anomaly :as ba] - [blaze.fhir.spec.type :as type])) + [blaze.fhir.spec.type :as type] + [blaze.luid :as luid])) -(defn expression [population-path-fn criteria] +(defn expression-name [population-path-fn criteria] (let [language (-> criteria :language type/value) expression (-> criteria :expression type/value)] (cond @@ -28,13 +29,13 @@ :else expression))) -(defn- list-reference [list-id] +(defn list-reference [list-id] (type/map->Reference {:reference (str "List/" list-id)})) (defn- resource-reference [{:keys [id] :as resource}] (type/map->Reference {:reference (str (name (type/type resource)) "/" id)})) -(defn- population-tx-ops [list-id handles] +(defn population-tx-ops [list-id handles] [[:create {:fhir/type :fhir/List :id list-id @@ -47,45 +48,13 @@ :item (resource-reference population-handle)}) handles)}]]) -(defn population [{:keys [report-type luids]} fhir-type code handles] - (case report-type - ("population" "subject") - {:result - (cond-> - {:fhir/type fhir-type - :count (count handles)} - code - (assoc :code code)) - :luids luids} - "subject-list" - (let [list-id (first luids)] - {:result - (cond-> - {:fhir/type fhir-type - :count (count handles) - :subjectResults (list-reference list-id)} - code - (assoc :code code)) - :luids (next luids) - :tx-ops (population-tx-ops list-id handles)}))) - -(defn population-count [{:keys [luids]} fhir-type code count] - {:result - (cond-> - {:fhir/type fhir-type - :count count} - code - (assoc :code code)) - :luids luids}) - (defn- merge-result* "Merges `result` into the return value of the reduction `ret`." {:arglists '([ret result])} - [ret {:keys [result handles luids tx-ops]}] - (cond-> (-> (update ret :result conj result) - (update :handles conj handles)) - luids - (assoc :luids luids) + [ret {:keys [result tx-ops] ::luid/keys [generator]}] + (cond-> (update ret :result conj result) + generator + (assoc ::luid/generator generator) (seq tx-ops) (update :tx-ops into tx-ops))) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql/spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql/spec.clj index 12bf78be6..f0d4dd6c3 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql/spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql/spec.clj @@ -1,6 +1,8 @@ (ns blaze.fhir.operation.evaluate-measure.cql.spec (:require - [blaze.elm.compiler :as-alias compiler] + [blaze.elm.compiler :as-alias c] + [blaze.elm.compiler.external-data :as ed] + [blaze.elm.compiler.library.spec] [blaze.elm.expression :as-alias expr] [blaze.fhir.operation.evaluate-measure :as-alias evaluate-measure] [blaze.fhir.operation.evaluate-measure.cql :as-alias cql] @@ -18,10 +20,13 @@ (s/merge ::expr/context (s/keys :req-un [::cql/timeout-eclipsed? ::cql/timeout - ::compiler/expression-defs]))) + ::c/expression-defs]))) -(s/def ::cql/return-handles? - boolean?) +(s/def ::cql/reduce-op + fn?) + +(s/def ::cql/combine-op + fn?) (s/def ::cql/population-basis (s/nilable :fhir.resource/type)) @@ -29,5 +34,18 @@ (s/def ::cql/evaluate-expression-context (s/merge ::cql/context - (s/keys :req-un [::evaluate-measure/executor] - :opt-un [::cql/return-handles? ::cql/population-basis]))) + (s/keys :req-un [::evaluate-measure/executor ::cql/reduce-op ::cql/combine-op] + :opt-un [::cql/population-basis]))) + +(s/def ::cql/subject-handle + ed/resource?) + +(s/def ::cql/population-handle + ed/resource?) + +(s/def ::cql/handle + (s/keys :req-un [::cql/subject-handle ::cql/population-handle])) + +(s/def ::cql/stratum-expression-evaluator-context + (s/keys :opt-un [::c/expression-defs ::c/function-defs + ::cql/population-basis])) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj index ccaec0144..382bbf29d 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/cql_spec.clj @@ -1,17 +1,24 @@ (ns blaze.fhir.operation.evaluate-measure.cql-spec (:require [blaze.async.comp :as ac] - [blaze.db.spec] + [blaze.elm.compiler :as-alias c] [blaze.elm.compiler.external-data :as ed] [blaze.elm.compiler.external-data-spec] - [blaze.elm.compiler.library-spec] - [blaze.elm.expression-spec] + [blaze.elm.compiler.spec] + [blaze.elm.expression :as-alias expr] + [blaze.elm.expression.spec] [blaze.fhir.operation.evaluate-measure.cql :as cql] [blaze.fhir.operation.evaluate-measure.cql.spec] - [blaze.fhir.operation.evaluate-measure.measure :as-alias measure] - [blaze.fhir.operation.evaluate-measure.measure.spec] [blaze.fhir.spec] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) + +(s/fdef cql/evaluate-expression-1 + :args (s/cat :context ::expr/context + :subject (s/nilable ed/resource?) + :name string? + :expression ::c/expression) + :ret ac/completable-future?) (s/fdef cql/evaluate-expression :args (s/cat :context ::cql/evaluate-expression-context :name string? @@ -24,20 +31,12 @@ :name string?) :ret ac/completable-future?) -(s/fdef cql/calc-strata - :args (s/cat :context ::cql/context - :expression-name string? - :handles ::measure/handles) - :ret ac/completable-future?) - -(s/fdef cql/calc-function-strata - :args (s/cat :context ::cql/context - :function-name string? - :handles ::measure/handles) - :ret ac/completable-future?) +(s/fdef cql/stratum-expression-evaluator + :args (s/cat :context ::cql/stratum-expression-evaluator-context + :name string?) + :ret (s/or :evaluator fn? :anomaly ::anom/anomaly)) -(s/fdef cql/calc-multi-component-strata - :args (s/cat :context ::cql/context - :expression-names (s/coll-of string?) - :handles ::measure/handles) - :ret ac/completable-future?) +(s/fdef cql/stratum-expression-evaluators + :args (s/cat :context ::cql/stratum-expression-evaluator-context + :name (s/coll-of string?)) + :ret (s/or :evaluators (s/coll-of fn?) :anomaly ::anom/anomaly)) 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 f335f25d0..bcb75f50c 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 @@ -83,6 +83,16 @@ define function Status(encounter Encounter): encounter.status") +(def library-specimen + "library Retrieve + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Specimen + + define InInitialPopulation: + true") + (defn- compile-library [node cql] (when-ok [library (cql-translator/translate cql)] (library/compile-library node library {}))) @@ -104,6 +114,15 @@ (def ^:private config (assoc mem-node-config :blaze.test/executor {})) +(def ^:private conj-reduce-op + (fn [_db] conj)) + +(def ^:private count-reduce-op + (fn [_db] ((map (constantly 1)) +))) + +(defn with-ops [context reduce-op combine-op] + (assoc context :reduce-op reduce-op :combine-op combine-op)) + (deftest evaluate-expression-test (testing "finds the male patient" (with-system-data [system config] @@ -113,7 +132,7 @@ (let [context (context system library-gender)] (testing "returning handles" - (let [context (assoc context :return-handles? true)] + (let [context (with-ops context conj-reduce-op into)] (given @(cql/evaluate-expression context "InInitialPopulation" "Patient") count := 1 [0 :population-handle fhir-spec/fhir-type] := :fhir/Patient @@ -122,7 +141,7 @@ [0 :subject-handle :id] := "1"))) (testing "not returning handles" - (let [context (assoc context :return-handles? false)] + (let [context (with-ops context count-reduce-op +)] (is (= 1 @(cql/evaluate-expression context "InInitialPopulation" "Patient")))))))) (testing "returns all encounters" @@ -136,7 +155,8 @@ (let [context (context system library-encounter)] (testing "returning handles" - (let [context (assoc context :return-handles? true :population-basis "Encounter")] + (let [context (-> (with-ops context conj-reduce-op into) + (assoc :population-basis "Encounter"))] (given @(cql/evaluate-expression context "InInitialPopulation" "Patient") count := 3 [0 :population-handle fhir-spec/fhir-type] := :fhir/Encounter @@ -153,64 +173,83 @@ [2 :subject-handle :id] := "1"))) (testing "not returning handles" - (let [context (assoc context :return-handles? false :population-basis "Encounter")] + (let [context (-> (with-ops context count-reduce-op +) + (assoc :population-basis "Encounter"))] (is (= 3 @(cql/evaluate-expression context "InInitialPopulation" "Patient")))))))) (testing "missing expression" (with-system [system config] - (let [context (context system library-empty)] - (doseq [return-handles? [true false] - :let [context (assoc context :return-handles? return-handles?)]] - (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") - ::anom/category := ::anom/incorrect - ::anom/message := "Missing expression with name `InInitialPopulation`." - :expression-name := "InInitialPopulation"))))) + (let [context (with-ops (context system library-empty) conj-reduce-op into)] + (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") + ::anom/category := ::anom/incorrect + ::anom/message := "Missing expression with name `InInitialPopulation`." + :expression-name := "InInitialPopulation")))) (testing "expression context doesn't match the subject type" (with-system [system config] - (let [context (context system library-gender)] - (doseq [return-handles? [true false] - :let [context (assoc context :return-handles? return-handles?)]] - (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Encounter") - ::anom/category := ::anom/incorrect - ::anom/message := "The context `Patient` of the expression `InInitialPopulation` differs from the subject type `Encounter`." - :expression-name := "InInitialPopulation" - :subject-type := "Encounter" - :expression-context := "Patient"))))) + (let [context (with-ops (context system library-gender) conj-reduce-op into)] + (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Encounter") + ::anom/category := ::anom/incorrect + ::anom/message := "The context `Patient` of the expression `InInitialPopulation` differs from the subject type `Encounter`." + :expression-name := "InInitialPopulation" + :subject-type := "Encounter" + :expression-context := "Patient")))) (testing "population basis doesn't match the expression return type" (testing "Boolean" (with-system [system config] - (let [context (context system library-encounter)] - (doseq [return-handles? [true false] - :let [context (assoc context :return-handles? return-handles?)]] - (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") - ::anom/category := ::anom/incorrect - ::anom/message := "The result type `List` of the expression `InInitialPopulation` differs from the population basis :boolean." - :expression-name := "InInitialPopulation" - :population-basis := :boolean - :expression-result-type := "List"))))) + (let [context (with-ops (context system library-encounter) conj-reduce-op into)] + (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") + ::anom/category := ::anom/incorrect + ::anom/message := "The result type `List` of the expression `InInitialPopulation` differs from the population basis :boolean." + :expression-name := "InInitialPopulation" + :population-basis := :boolean + :expression-result-type := "List")))) (testing "Encounter" (with-system [system config] - (let [context (context system library-gender)] - (doseq [return-handles? [true false] - :let [context (assoc context :return-handles? return-handles? :population-basis "Encounter")]] - (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") - ::anom/category := ::anom/incorrect - ::anom/message := "The result type `Boolean` of the expression `InInitialPopulation` differs from the population basis `Encounter`." - :expression-name := "InInitialPopulation" - :population-basis := "Encounter" - :expression-result-type := "Boolean")))))) + (let [context (-> (context system library-gender) + (with-ops conj-reduce-op into) + (assoc :population-basis "Encounter"))] + (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") + ::anom/category := ::anom/incorrect + ::anom/message := "The result type `Boolean` of the expression `InInitialPopulation` differs from the population basis `Encounter`." + :expression-name := "InInitialPopulation" + :population-basis := "Encounter" + :expression-result-type := "Boolean"))))) + + (testing "finds the specimen" + (with-system-data [system config] + [[[:put {:fhir/type :fhir/Specimen :id "0"}]]] + + (let [context (context system library-specimen)] + (testing "returning handles" + (let [context (with-ops context conj-reduce-op into)] + (given @(cql/evaluate-expression context "InInitialPopulation" "Specimen") + count := 1 + [0 :population-handle fhir-spec/fhir-type] := :fhir/Specimen + [0 :population-handle :id] := "0"))) + + (testing "not returning handles" + (let [context (with-ops context count-reduce-op +)] + (is (= 1 @(cql/evaluate-expression context "InInitialPopulation" "Specimen")))))))) (testing "failing eval" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [context (context system library-gender)] - (with-redefs [expr/eval (failing-eval "msg-222453")] - (doseq [return-handles? [true false] - :let [context (assoc context :return-handles? return-handles?)]] + (testing "subject-based" + (let [context (with-ops (context system library-gender) conj-reduce-op into)] + (with-redefs [expr/eval (failing-eval "msg-222453")] + (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") + ::anom/category := ::anom/fault + ::anom/message := "Error while evaluating the expression `InInitialPopulation`: msg-222453")))) + + (testing "population-based" + (let [context (-> (context system library-encounter) + (with-ops conj-reduce-op into) + (assoc :population-basis "Encounter"))] + (with-redefs [expr/eval (failing-eval "msg-222453")] (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") ::anom/category := ::anom/fault ::anom/message := "Error while evaluating the expression `InInitialPopulation`: msg-222453")))))) @@ -219,26 +258,29 @@ (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [context (assoc (context system library-gender) :timeout-eclipsed? (constantly true))] - (doseq [return-handles? [true false] - :let [context (assoc context :return-handles? return-handles?)]] - (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") - ::anom/category := ::anom/interrupted - ::anom/message := "Timeout of 42000 millis eclipsed while evaluating.")))))) + (let [context (-> (context system library-gender) + (with-ops conj-reduce-op into) + (assoc :timeout-eclipsed? (constantly true)))] + + (given-failed-future (cql/evaluate-expression context "InInitialPopulation" "Patient") + ::anom/category := ::anom/interrupted + ::anom/message := "Timeout of 42000 millis eclipsed while evaluating."))))) (deftest evaluate-individual-expression-test (testing "counting" (testing "match" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}]]] - (let [{:keys [db] :as context} (context system library-gender) + (let [{:keys [db] :as context} (-> (context system library-gender) + (with-ops count-reduce-op +)) patient (em-tu/resource db "Patient" "0")] (is (= 1 @(cql/evaluate-individual-expression context patient "InInitialPopulation")))))) (testing "no match" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [{:keys [db] :as context} (context system library-gender) + (let [{:keys [db] :as context} (-> (context system library-gender) + (with-ops count-reduce-op +)) patient (em-tu/resource db "Patient" "0")] (is (zero? @(cql/evaluate-individual-expression context patient "InInitialPopulation"))))))) @@ -246,8 +288,10 @@ (testing "match" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0" :gender #fhir/code"male"}]]] - (let [{:keys [db] :as context} (assoc (context system library-gender) :return-handles? true) + (let [{:keys [db] :as context} (-> (context system library-gender) + (with-ops conj-reduce-op into)) patient (em-tu/resource db "Patient" "0")] + (given @(cql/evaluate-individual-expression context patient "InInitialPopulation") count := 1 [0 :population-handle fhir-spec/fhir-type] := :fhir/Patient @@ -258,15 +302,18 @@ (testing "no match" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [{:keys [db] :as context} (assoc (context system library-gender) :return-handles? true) + (let [{:keys [db] :as context} (-> (context system library-gender) + (with-ops conj-reduce-op into)) patient (em-tu/resource db "Patient" "0")] (is (empty? @(cql/evaluate-individual-expression context patient "InInitialPopulation"))))))) (testing "missing expression" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [{:keys [db] :as context} (context system library-empty) + (let [{:keys [db] :as context} (-> (context system library-empty) + (with-ops conj-reduce-op into)) patient (em-tu/resource db "Patient" "0")] + (given-failed-future (cql/evaluate-individual-expression context patient "InInitialPopulation") ::anom/category := ::anom/incorrect ::anom/message := "Missing expression with name `InInitialPopulation`." @@ -275,9 +322,11 @@ (testing "error" (with-system-data [system config] [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [{:keys [db] :as context} (assoc (context system library-error) - :parameters {"Numbers" [1 2]}) + (let [{:keys [db] :as context} (-> (context system library-error) + (with-ops conj-reduce-op into) + (assoc :parameters {"Numbers" [1 2]})) patient (em-tu/resource db "Patient" "0")] + (given-failed-future (cql/evaluate-individual-expression context patient "InInitialPopulation") ::anom/category := ::anom/conflict ::anom/message := "More than one element in `SingletonFrom` expression." @@ -285,156 +334,28 @@ :expression-name := "InInitialPopulation" :list := [1 2]))))) -(def two-value-eval - (fn [_ _ _] ["1" "2"])) - -(deftest calc-strata-test +(deftest stratum-evaluator-test (testing "missing expression" - (with-system [system config] - (let [context (context system library-empty)] - (given-failed-future (cql/calc-strata context "Gender" []) - ::anom/category := ::anom/incorrect - ::anom/message := "Missing expression with name `Gender`." - :expression-name := "Gender")))) - - (testing "failing eval" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - - (let [{:keys [db] :as context} (context system library-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] - (with-redefs [expr/eval (failing-eval "msg-221825")] - (given-failed-future (cql/calc-strata context "Gender" handles) - ::anom/category := ::anom/fault - ::anom/message := "Error while evaluating the expression `Gender`: msg-221825"))))) - - (testing "multiple values" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - - (let [{:keys [db] :as context} (context system library-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] - (with-redefs [expr/eval two-value-eval] - (given-failed-future (cql/calc-strata context "Gender" handles) - ::anom/category := ::anom/incorrect - ::anom/message := "CQL expression `Gender` returned more than one value for resource `Patient/0`."))))) - - (testing "timeout eclipsed" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - - (let [{:keys [db] :as context} (assoc (context system library-gender) :timeout-eclipsed? (constantly true)) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] - (given-failed-future (cql/calc-strata context "Gender" handles) - ::anom/category := ::anom/interrupted - ::anom/message := "Timeout of 42000 millis eclipsed while evaluating.")))) - - (testing "gender" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}] - [:put {:fhir/type :fhir/Patient :id "1" :gender #fhir/code"male"}] - [:put {:fhir/type :fhir/Patient :id "2" :gender #fhir/code"female"}] - [:put {:fhir/type :fhir/Patient :id "3" :gender #fhir/code"male"}]]] - - (let [{:keys [db] :as context} (context system library-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient")) - result @(cql/calc-strata context "Gender" handles)] + (given (cql/stratum-expression-evaluator {} "foo") + ::anom/category := ::anom/incorrect + ::anom/message := "Missing expression with name `foo`." + :expression-name := "foo")) - (testing "contains a nil entry for the patient with id 0" - (given (result nil) - count := 1 - [0 :subject-handle :id] := "0" - [0 :population-handle :id] := "0")) - - (testing "contains a male entry for the patients with id 1 and 3" - (given (result #fhir/code"male") - count := 2 - [0 :subject-handle :id] := "1" - [0 :population-handle :id] := "1" - [1 :subject-handle :id] := "3" - [1 :population-handle :id] := "3")) - - (testing "contains a female entry for the patient with id 2" - (given (result #fhir/code"female") - count := 1 - [0 :subject-handle :id] := "2" - [0 :population-handle :id] := "2")))))) + (testing "missing function" + (given (cql/stratum-expression-evaluator {:population-basis "Encounter"} "foo") + ::anom/category := ::anom/incorrect + ::anom/message := "Missing function with name `foo`." + :function-name := "foo"))) -(deftest calc-function-strata-test - (testing "Encounter status" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}] - [:put {:fhir/type :fhir/Patient :id "1"}] - [:put {:fhir/type :fhir/Patient :id "2"}] - [:put {:fhir/type :fhir/Encounter :id "0" - :subject #fhir/Reference{:reference "Patient/0"}}] - [:put {:fhir/type :fhir/Encounter :id "1" - :status #fhir/code"finished" - :subject #fhir/Reference{:reference "Patient/0"}}] - [:put {:fhir/type :fhir/Encounter :id "2" - :status #fhir/code"planned" - :subject #fhir/Reference{:reference "Patient/1"}}] - [:put {:fhir/type :fhir/Encounter :id "3" - :status #fhir/code"finished" - :subject #fhir/Reference{:reference "Patient/2"}}]]] - - (let [{:keys [db] :as context} (context system library-encounter-status) - handles - [{:population-handle (em-tu/resource db "Encounter" "0") - :subject-handle (em-tu/resource db "Patient" "0")} - {:population-handle (em-tu/resource db "Encounter" "1") - :subject-handle (em-tu/resource db "Patient" "0")} - {:population-handle (em-tu/resource db "Encounter" "2") - :subject-handle (em-tu/resource db "Patient" "1")} - {:population-handle (em-tu/resource db "Encounter" "3") - :subject-handle (em-tu/resource db "Patient" "2")}] - result @(cql/calc-function-strata context "Status" handles)] - - (testing "contains a nil entry for the encounter with id 0" - (given (result nil) - count := 1 - [0 :population-handle :id] := "0" - [0 :subject-handle :id] := "0")) - - (testing "contains a finished entry for the encounters with id 1 and 3" - (given (result #fhir/code"finished") - count := 2 - [0 :population-handle :id] := "1" - [0 :subject-handle :id] := "0" - [1 :population-handle :id] := "3" - [1 :subject-handle :id] := "2")) - - (testing "contains a planned entry for the encounter with id 2" - (given (result #fhir/code"planned") - count := 1 - [0 :population-handle :id] := "2" - [0 :subject-handle :id] := "1"))))) +(deftest stratum-evaluators-test + (testing "missing expression" + (given (cql/stratum-expression-evaluators {} ["foo"]) + ::anom/category := ::anom/incorrect + ::anom/message := "Missing expression with name `foo`." + :expression-name := "foo")) (testing "missing function" - (with-system [system config] - (let [context (context system library-empty)] - (given-failed-future (cql/calc-function-strata context "Gender" []) - ::anom/category := ::anom/incorrect - ::anom/message := "Missing function with name `Gender`." - :function-name := "Gender")))) - - (testing "failing eval" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [{:keys [db] :as context} (context system library-encounter-status) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] - (with-redefs [expr/eval (failing-eval "msg-111807")] - (given-failed-future (cql/calc-function-strata context "Status" handles) - ::anom/category := ::anom/fault - ::anom/message := "Error while evaluating the expression `Status`: msg-111807")))))) - -(deftest calc-multi-component-strata-test - (testing "failing eval" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - (let [{:keys [db] :as context} (context system library-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] - (with-redefs [expr/eval (failing-eval "msg-111557")] - (given-failed-future (cql/calc-multi-component-strata context ["Gender"] handles) - ::anom/category := ::anom/fault - ::anom/message := "Error while evaluating the expression `Gender`: msg-111557")))))) + (given (cql/stratum-expression-evaluators {:population-basis "Encounter"} ["foo"]) + ::anom/category := ::anom/incorrect + ::anom/message := "Missing function with name `foo`." + :function-name := "foo"))) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/group_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/group_spec.clj new file mode 100644 index 000000000..c5d66dd18 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/group_spec.clj @@ -0,0 +1,29 @@ +(ns blaze.fhir.operation.evaluate-measure.measure.group-spec + (:require + [blaze.fhir.operation.evaluate-measure.measure.group :as group] + [blaze.fhir.spec.spec] + [blaze.luid :as-alias luid] + [blaze.luid.spec] + [clojure.spec.alpha :as s])) + +(s/fdef group/combine-op-count + :args (s/cat :luid-generator ::luid/generator + :code (s/nilable :fhir/CodeableConcept)) + :ret fn?) + +(s/fdef group/combine-op-count-stratifier + :args (s/cat :luid-generator ::luid/generator + :code (s/nilable :fhir/CodeableConcept) + :stratifiers (s/coll-of :fhir.Measure.group/stratifier)) + :ret fn?) + +(s/fdef group/combine-op-subject-list + :args (s/cat :luid-generator ::luid/generator + :code (s/nilable :fhir/CodeableConcept)) + :ret fn?) + +(s/fdef group/combine-op-subject-list-stratifier + :args (s/cat :luid-generator ::luid/generator + :code (s/nilable :fhir/CodeableConcept) + :stratifiers (s/coll-of :fhir.Measure.group/stratifier)) + :ret fn?) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population/spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population/spec.clj index 0de189f3b..c233a81db 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population/spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population/spec.clj @@ -1,20 +1,19 @@ (ns blaze.fhir.operation.evaluate-measure.measure.population.spec (:require [blaze.db.spec] - [blaze.elm.compiler.external-data :as ed] [blaze.fhir.operation.evaluate-measure.cql :as-alias cql] [blaze.fhir.operation.evaluate-measure.cql.spec] [blaze.fhir.operation.evaluate-measure.measure.population :as-alias population] [blaze.fhir.spec.spec] + [blaze.luid :as-alias luid] + [blaze.luid.spec] [clojure.spec.alpha :as s])) (s/def ::population/subject-type :fhir.resource/type) -(s/def ::population/subject-handle - ed/resource?) - (s/def ::population/context (s/merge ::cql/context - (s/keys :req-un [(or ::population/subject-type ::population/subject-handle)]))) + (s/keys :req [::luid/generator] + :req-un [(or ::population/subject-type ::cql/subject-handle)]))) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population_spec.clj index 401af5c90..e9b807e6e 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/population_spec.clj @@ -3,8 +3,13 @@ [blaze.async.comp :as ac] [blaze.fhir.operation.evaluate-measure.measure.population :as population] [blaze.fhir.operation.evaluate-measure.measure.population.spec] + [blaze.fhir.spec.spec] [clojure.spec.alpha :as s])) (s/fdef population/evaluate :args (s/cat :context ::population/context :idx nat-int? :population map?) :ret ac/completable-future?) + +(s/fdef population/population + :args (s/cat :code :fhir/CodeableConcept :count int?) + :ret :fhir.Measure.group/population) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier/spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier/spec.clj index 0e442abff..7537b55f4 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier/spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier/spec.clj @@ -4,16 +4,16 @@ [blaze.fhir.operation.evaluate-measure.cql :as-alias cql] [blaze.fhir.operation.evaluate-measure.cql.spec] [blaze.fhir.operation.evaluate-measure.measure :as-alias measure] - [blaze.fhir.operation.evaluate-measure.measure.stratifier :as-alias stratifier] + [blaze.fhir.operation.evaluate-measure.measure.stratifier :as-alias strat] [blaze.fhir.operation.evaluate-measure.spec] [clojure.spec.alpha :as s])) -(s/def ::stratifier/handles +(s/def ::strat/handles (s/coll-of ::measure/handles)) -(s/def ::stratifier/evaluated-populations - (s/keys :req-un [::stratifier/handles])) +(s/def ::strat/evaluated-populations + (s/keys :req-un [::strat/handles])) -(s/def ::stratifier/context +(s/def ::strat/context (s/merge ::cql/context (s/keys :req-un [::evaluate-measure/executor ::measure/report-type]))) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_spec.clj index 530d0b703..1f09c7b4e 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/stratifier_spec.clj @@ -1,15 +1,33 @@ (ns blaze.fhir.operation.evaluate-measure.measure.stratifier-spec (:require - [blaze.async.comp :as ac] + [blaze.db.spec] + [blaze.fhir.operation.evaluate-measure.cql :as-alias cql] [blaze.fhir.operation.evaluate-measure.cql-spec] + [blaze.fhir.operation.evaluate-measure.cql.spec] [blaze.fhir.operation.evaluate-measure.measure.spec] - [blaze.fhir.operation.evaluate-measure.measure.stratifier :as stratifier] + [blaze.fhir.operation.evaluate-measure.measure.stratifier :as strat] [blaze.fhir.operation.evaluate-measure.measure.stratifier.spec] [blaze.fhir.operation.evaluate-measure.measure.util-spec] - [clojure.spec.alpha :as s])) + [blaze.fhir.spec.spec] + [blaze.luid :as-alias luid] + [blaze.luid.spec] + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) -(s/fdef stratifier/evaluate - :args (s/cat :context ::stratifier/context - :evaluated-populations ::stratifier/evaluated-populations - :stratifier map?) - :ret ac/completable-future?) +(s/fdef strat/stratum-subject-list + :args (s/cat :context (s/keys :req [::luid/generator] :req-un [:blaze.db/tx-ops]) + :value some? + :populations (s/map-of :fhir/CodeableConcept (s/coll-of ::cql/handle))) + :ret map?) + +(s/fdef strat/multi-component-stratum-subject-list + :args (s/cat :codes (s/coll-of :fhir/CodeableConcept) + :context (s/keys :req [::luid/generator] :req-un [:blaze.db/tx-ops]) + :values vector? + :populations (s/map-of :fhir/CodeableConcept (s/coll-of ::cql/handle))) + :ret map?) + +(s/fdef strat/reduce-op + :args (s/cat :context ::strat/context + :stratifier :fhir.Measure.group/stratifier) + :ret (s/or :reduce-op fn? :anomaly ::anom/anomaly)) 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 223d66874..b125301bd 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,14 +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.fhir.operation.evaluate-measure.measure.stratifier :as stratifier] + [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.fhir.test-util :refer [given-failed-future]] [blaze.module.test-util :refer [with-system]] [blaze.test-util :as tu] [clojure.spec.test.alpha :as st] - [clojure.test :as test :refer [deftest testing]] + [clojure.test :as test :refer [deftest is testing]] [cognitect.anomalies :as anom] [java-time.api :as time] [juxt.iota :refer [given]]) @@ -160,19 +159,21 @@ (defn- context [{:blaze.db/keys [node] :blaze.test/keys [fixed-clock executor]} library] (let [{:keys [expression-defs function-defs]} (compile-library node library)] - {:db (d/db node) - :now (now fixed-clock) - :timeout-eclipsed? (constantly false) - :timeout (time/seconds 42) - :expression-defs expression-defs - :function-defs function-defs - :executor executor})) + (cond-> + {:db (d/db node) + :now (now fixed-clock) + :timeout-eclipsed? (constantly false) + :timeout (time/seconds 42) + :expression-defs expression-defs + :executor executor} + function-defs + (assoc :function-defs function-defs)))) (def ^:private config (assoc mem-node-config :blaze.test/executor {})) -(deftest evaluate +(deftest reduce-op-test (testing "one component" (testing "gender" (with-system-data [system config] @@ -182,48 +183,25 @@ [:put {:fhir/type :fhir/Patient :id "3" :gender #fhir/code"male"}]]] (let [{:keys [db] :as context} (context system library-age-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient")) - evaluated-populations {:handles [handles]}] + handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] (testing "report-type population" - (given @(stratifier/evaluate (assoc context :report-type "population") - evaluated-populations gender-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"gender" - [:result :stratum 0 :value :text] := #fhir/string"female" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1 - [:result :stratum 1 :value :text] := #fhir/string"male" - [:result :stratum 1 :population 0 :count] := #fhir/integer 2 - [:result :stratum 2 :value :text] := #fhir/string"null" - [:result :stratum 2 :population 0 :count] := #fhir/integer 1)) + (let [op ((strat/reduce-op + (assoc context :report-type "population") + gender-stratifier) db)] + (is (= (reduce op (op) handles) + {nil 1 + #fhir/code"male" 2 + #fhir/code"female" 1})))) (testing "report-type subject-list" - (given @(stratifier/evaluate - (assoc context - :luids ["L0" "L1" "L2"] - :report-type "subject-list") - evaluated-populations gender-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"gender" - [:result :stratum 0 :value :text] := #fhir/string"female" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1 - [:result :stratum 0 :population 0 :subjectResults :reference] := "List/L2" - [:result :stratum 1 :value :text] := #fhir/string"male" - [:result :stratum 1 :population 0 :count] := #fhir/integer 2 - [:result :stratum 1 :population 0 :subjectResults :reference] := "List/L1" - [:result :stratum 2 :value :text] := #fhir/string"null" - [:result :stratum 2 :population 0 :count] := #fhir/integer 1 - [:result :stratum 2 :population 0 :subjectResults :reference] := "List/L0" - [:tx-ops 0 0] := :create - [:tx-ops 0 1 :fhir/type] := :fhir/List - [:tx-ops 0 1 :id] := "L0" - [:tx-ops 0 1 :entry 0 :item :reference] := "Patient/0" - [:tx-ops 1 1 :id] := "L1" - [:tx-ops 1 1 :entry 0 :item :reference] := "Patient/1" - [:tx-ops 1 1 :entry 1 :item :reference] := "Patient/3" - [:tx-ops 2 1 :id] := "L2" - [:tx-ops 2 1 :entry 0 :item :reference] := "Patient/2" - [:tx-ops count] := 3))))) + (let [op ((strat/reduce-op + (assoc context :report-type "subject-list") + gender-stratifier) db)] + (is (= (reduce op (op) handles) + {nil [(nth handles 0)] + #fhir/code"male" [(nth handles 1) (nth handles 3)] + #fhir/code"female" [(nth handles 2)]}))))))) (testing "CodeableConcept" (with-system-data [system config] @@ -234,23 +212,23 @@ [#fhir/Coding{:system #fhir/uri"http://loinc.org" :code #fhir/code"17861-6"}]} :subject #fhir/Reference{:reference "Patient/0"}}]]] + (let [{:keys [db] :as context} (context system library-observation-code) - evaluated-populations - {:handles - [[{:population-handle (em-tu/resource db "Observation" "0") - :subject-handle (em-tu/resource db "Patient" "0")}]]}] + handles [{:population-handle (em-tu/resource db "Observation" "0") + :subject-handle (em-tu/resource db "Patient" "0")}]] (testing "report-type population" - (given @(stratifier/evaluate - (assoc context - :report-type "population" - :population-basis "Observation") - evaluated-populations observation-code-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"code" - [:result :stratum 0 :value :coding 0 :system] := #fhir/uri"http://loinc.org" - [:result :stratum 0 :value :coding 0 :code] := #fhir/code"17861-6" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1))))) + (let [op ((strat/reduce-op + (assoc context + :report-type "population" + :population-basis "Observation") + observation-code-stratifier) db)] + (is (= (reduce op (op) handles) + {#fhir/CodeableConcept + {:coding + [#fhir/Coding{:system #fhir/uri"http://loinc.org" + :code #fhir/code"17861-6"}]} + 1}))))))) (testing "Quantity" (with-system-data [system config] @@ -261,47 +239,42 @@ {:value #fhir/decimal 1M :code #fhir/code"kg"}}] [:put {:fhir/type :fhir/Observation :id "1" + :subject #fhir/Reference{:reference "Patient/0"} + :value #fhir/Quantity + {:value #fhir/decimal 2M}}] + [:put {:fhir/type :fhir/Observation :id "2" :subject #fhir/Reference{:reference "Patient/0"} :value #fhir/Quantity {:value #fhir/decimal 2M}}]]] + (let [{:keys [db] :as context} (context system library-observation-value-age) - evaluated-populations - {:handles - [[{:population-handle (em-tu/resource db "Observation" "0") - :subject-handle (em-tu/resource db "Patient" "0")} - {:population-handle (em-tu/resource db "Observation" "1") - :subject-handle (em-tu/resource db "Patient" "0")}]]}] + handles [{:population-handle (em-tu/resource db "Observation" "0") + :subject-handle (em-tu/resource db "Patient" "0")} + {:population-handle (em-tu/resource db "Observation" "1") + :subject-handle (em-tu/resource db "Patient" "0")} + {:population-handle (em-tu/resource db "Observation" "2") + :subject-handle (em-tu/resource db "Patient" "0")}]] (testing "report-type population" - (given @(stratifier/evaluate - (assoc context - :report-type "population" - :population-basis "Observation") - evaluated-populations observation-value-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"value" - [:result :stratum 0 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.value" - [:result :stratum 0 :extension 0 :value :value] := #fhir/decimal 1M - [:result :stratum 0 :extension 0 :value :code] := #fhir/code"kg" - [:result :stratum 0 :value :text] := "1 kg" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1 - [:result :stratum 1 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.value" - [:result :stratum 1 :extension 0 :value :value] := #fhir/decimal 2M - [:result :stratum 1 :value :text] := "2" - [:result :stratum 1 :population 0 :count] := #fhir/integer 1))))) + (let [op ((strat/reduce-op + (assoc context + :report-type "population" + :population-basis "Observation") + observation-value-stratifier) db)] + (is (= (reduce op (op) handles) + {#fhir/Quantity{:value #fhir/decimal 1M :code #fhir/code"kg"} 1 + #fhir/Quantity{:value #fhir/decimal 2M} 2}))))))) (testing "errors" (testing "with expression" (with-system [system config] - (let [context (context system empty-library) - evaluated-populations {:handles [[]]}] - (given-failed-future (stratifier/evaluate - (assoc context - :report-type "population" - :group-idx 1 - :stratifier-idx 2) - evaluated-populations - stratifier-with-missing-expression) + (let [context (context system empty-library)] + (given (strat/reduce-op + (assoc context + :report-type "population" + :group-idx 1 + :stratifier-idx 2) + stratifier-with-missing-expression) ::anom/category := ::anom/incorrect ::anom/message := "Missing expression." :fhir/issue := "required" @@ -309,28 +282,12 @@ (testing "with unknown expression" (with-system [system config] - (let [context (context system empty-library) - evaluated-populations {:handles [[]]}] - (given-failed-future (stratifier/evaluate (assoc context :report-type "population") - evaluated-populations gender-stratifier) + (let [context (context system empty-library)] + (given (strat/reduce-op (assoc context :report-type "population") + gender-stratifier) ::anom/category := ::anom/incorrect ::anom/message := "Missing expression with name `Gender`." - :expression-name := "Gender")))) - - (testing "gender" - (with-system-data [system config] - [[[:put {:fhir/type :fhir/Patient :id "0"}]]] - - (let [{:keys [db] :as context} (context system library-age-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient")) - evaluated-populations {:handles [handles]}] - - (given-failed-future (stratifier/evaluate (assoc context - :report-type "population" - :timeout-eclipsed? (constantly true)) - evaluated-populations gender-stratifier) - ::anom/category := ::anom/interrupted - ::anom/message := "Timeout of 42000 millis eclipsed while evaluating.")))))) + :expression-name := "Gender")))))) (testing "two components" (testing "subject-based measure" @@ -345,30 +302,18 @@ [:put {:fhir/type :fhir/Patient :id "3" :gender #fhir/code"male" :birthDate #fhir/date"1950"}]]] + (let [{:keys [db] :as context} (context system library-age-gender) - handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient")) - evaluated-populations {:handles [handles]}] + handles (into [] (em-tu/handle-mapper db) (d/type-list db "Patient"))] (testing "report-type population" - (given @(stratifier/evaluate (assoc context :report-type "population") - evaluated-populations age-gender-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"age" - [:result :code 1 :text] := #fhir/string"gender" - [:result :stratum 0 :component 0 :code :text] := #fhir/string"age" - [:result :stratum 0 :component 0 :value :text] := #fhir/string"10" - [:result :stratum 0 :component 1 :code :text] := #fhir/string"gender" - [:result :stratum 0 :component 1 :value :text] := #fhir/string"female" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1 - [:result :stratum 1 :component 0 :value :text] := #fhir/string"10" - [:result :stratum 1 :component 1 :value :text] := #fhir/string"male" - [:result :stratum 1 :population 0 :count] := #fhir/integer 1 - [:result :stratum 2 :component 0 :value :text] := #fhir/string"20" - [:result :stratum 2 :component 1 :value :text] := #fhir/string"male" - [:result :stratum 2 :population 0 :count] := #fhir/integer 1 - [:result :stratum 3 :component 0 :value :text] := #fhir/string"null" - [:result :stratum 3 :component 1 :value :text] := #fhir/string"null" - [:result :stratum 3 :population 0 :count] := #fhir/integer 1))))) + (let [op ((strat/reduce-op (assoc context :report-type "population") + age-gender-stratifier) db)] + (is (= (reduce op (op) handles) + {[nil nil] 1 + [10 #fhir/code"female"] 1 + [10 #fhir/code"male"] 1 + [20 #fhir/code"male"] 1}))))))) (testing "Encounter measure" (with-system-data [system config] @@ -386,44 +331,37 @@ :subject #fhir/Reference{:reference "Patient/1"} :period #fhir/Period{:start #fhir/dateTime"2021"}}] [:put {:fhir/type :fhir/Encounter :id "3" + :status #fhir/code"finished" + :subject #fhir/Reference{:reference "Patient/2"} + :period #fhir/Period{:start #fhir/dateTime"2022"}}] + [:put {:fhir/type :fhir/Encounter :id "4" :status #fhir/code"finished" :subject #fhir/Reference{:reference "Patient/2"} :period #fhir/Period{:start #fhir/dateTime"2022"}}]]] + (let [{:keys [db] :as context} (context system library-encounter-status-age) - evaluated-populations - {:handles - [[{:population-handle (em-tu/resource db "Encounter" "0") - :subject-handle (em-tu/resource db "Patient" "0")} - {:population-handle (em-tu/resource db "Encounter" "1") - :subject-handle (em-tu/resource db "Patient" "0")} - {:population-handle (em-tu/resource db "Encounter" "2") - :subject-handle (em-tu/resource db "Patient" "1")} - {:population-handle (em-tu/resource db "Encounter" "3") - :subject-handle (em-tu/resource db "Patient" "2")}]]}] + handles [{:population-handle (em-tu/resource db "Encounter" "0") + :subject-handle (em-tu/resource db "Patient" "0")} + {:population-handle (em-tu/resource db "Encounter" "1") + :subject-handle (em-tu/resource db "Patient" "0")} + {:population-handle (em-tu/resource db "Encounter" "2") + :subject-handle (em-tu/resource db "Patient" "1")} + {:population-handle (em-tu/resource db "Encounter" "3") + :subject-handle (em-tu/resource db "Patient" "2")} + {:population-handle (em-tu/resource db "Encounter" "4") + :subject-handle (em-tu/resource db "Patient" "2")}]] (testing "report-type population" - (given @(stratifier/evaluate - (assoc context - :report-type "population" - :population-basis "Encounter") - evaluated-populations status-age-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"status" - [:result :code 1 :text] := #fhir/string"age" - [:result :stratum 0 :component 0 :code :text] := #fhir/string"status" - [:result :stratum 0 :component 0 :value :text] := #fhir/string"finished" - [:result :stratum 0 :component 1 :code :text] := #fhir/string"age" - [:result :stratum 0 :component 1 :value :text] := #fhir/string"19" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1 - [:result :stratum 1 :component 0 :value :text] := #fhir/string"finished" - [:result :stratum 1 :component 1 :value :text] := #fhir/string"20" - [:result :stratum 1 :population 0 :count] := #fhir/integer 1 - [:result :stratum 2 :component 0 :value :text] := #fhir/string"null" - [:result :stratum 2 :component 1 :value :text] := #fhir/string"null" - [:result :stratum 2 :population 0 :count] := #fhir/integer 1 - [:result :stratum 3 :component 0 :value :text] := #fhir/string"planned" - [:result :stratum 3 :component 1 :value :text] := #fhir/string"20" - [:result :stratum 3 :population 0 :count] := #fhir/integer 1))))) + (let [op ((strat/reduce-op + (assoc context + :report-type "population" + :population-basis "Encounter") + status-age-stratifier) db)] + (is (= (reduce op (op) handles) + {[nil nil] 1 + [#fhir/code"finished" 19] 2 + [#fhir/code"finished" 20] 1 + [#fhir/code"planned" 20] 1}))))))) (testing "Quantity" (with-system-data [system config] @@ -439,49 +377,33 @@ :effective #fhir/dateTime"2021" :value #fhir/Quantity {:value #fhir/decimal 2M}}]]] + (let [{:keys [db] :as context} (context system library-observation-value-age) - evaluated-populations - {:handles - [[{:population-handle (em-tu/resource db "Observation" "0") - :subject-handle (em-tu/resource db "Patient" "0")} - {:population-handle (em-tu/resource db "Observation" "1") - :subject-handle (em-tu/resource db "Patient" "0")}]]}] + handles [{:population-handle (em-tu/resource db "Observation" "0") + :subject-handle (em-tu/resource db "Patient" "0")} + {:population-handle (em-tu/resource db "Observation" "1") + :subject-handle (em-tu/resource db "Patient" "0")}]] (testing "report-type population" - (given @(stratifier/evaluate - (assoc context - :report-type "population" - :population-basis "Observation") - evaluated-populations observation-value-age-stratifier) - [:result :fhir/type] := :fhir.MeasureReport.group/stratifier - [:result :code 0 :text] := #fhir/string"value" - [:result :code 1 :text] := #fhir/string"age" - [:result :stratum 0 :component 0 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.component.value" - [:result :stratum 0 :component 0 :extension 0 :value :value] := #fhir/decimal 1M - [:result :stratum 0 :component 0 :extension 0 :value :code] := #fhir/code"kg" - [:result :stratum 0 :component 0 :code :text] := #fhir/string"value" - [:result :stratum 0 :component 0 :value :text] := #fhir/string"1 kg" - [:result :stratum 0 :component 1 :code :text] := #fhir/string"age" - [:result :stratum 0 :component 1 :value :text] := #fhir/string"20" - [:result :stratum 0 :population 0 :count] := #fhir/integer 1 - [:result :stratum 1 :component 0 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.component.value" - [:result :stratum 1 :component 0 :extension 0 :value :value] := #fhir/decimal 2M - [:result :stratum 1 :component 0 :value :text] := #fhir/string"2" - [:result :stratum 1 :component 1 :value :text] := #fhir/string"21" - [:result :stratum 1 :population 0 :count] := #fhir/integer 1))))) + (let [op ((strat/reduce-op + (assoc context + :report-type "population" + :population-basis "Observation") + observation-value-age-stratifier) db)] + (is (= (reduce op (op) handles) + {[#fhir/Quantity{:value #fhir/decimal 1M :code #fhir/code"kg"} 20] 1 + [#fhir/Quantity{:value #fhir/decimal 2M} 21] 1}))))))) (testing "errors" (testing "with expression" (with-system [system config] - (let [context (context system empty-library) - evaluated-populations {:handles [[]]}] - (given-failed-future (stratifier/evaluate - (assoc context - :report-type "population" - :group-idx 1 - :stratifier-idx 2) - evaluated-populations - multi-component-stratifier-with-missing-expression) + (let [context (context system empty-library)] + (given (strat/reduce-op + (assoc context + :report-type "population" + :group-idx 1 + :stratifier-idx 2) + multi-component-stratifier-with-missing-expression) ::anom/category := ::anom/incorrect ::anom/message := "Missing expression." :fhir/issue := "required" @@ -489,10 +411,9 @@ (testing "with unknown expression" (with-system [system config] - (let [context (context system empty-library) - evaluated-populations {:handles [[]]}] - (given-failed-future (stratifier/evaluate (assoc context :report-type "population") - evaluated-populations age-gender-stratifier) + (let [context (context system empty-library)] + (given (strat/reduce-op (assoc context :report-type "population") + age-gender-stratifier) ::anom/category := ::anom/incorrect ::anom/message := "Missing expression with name `Age`." :expression-name := "Age"))))))) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj index 85b9b4b35..b8c6f6379 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_spec.clj @@ -1,13 +1,10 @@ (ns blaze.fhir.operation.evaluate-measure.measure.util-spec (:require - [blaze.fhir.operation.evaluate-measure.measure :as-alias measure] - [blaze.fhir.operation.evaluate-measure.measure.spec] [blaze.fhir.operation.evaluate-measure.measure.util :as u] - [clojure.spec.alpha :as s])) + [blaze.fhir.spec.spec] + [clojure.spec.alpha :as s] + [cognitect.anomalies :as anom])) -(s/fdef u/population - :args (s/cat :context map? :fhir-type :fhir/type :code any? - :handles ::measure/handles)) - -(s/fdef u/population-count - :args (s/cat :context map? :fhir-type :fhir/type :code any? :count int?)) +(s/fdef u/expression-name + :args (s/cat :population-path-fn fn? :criteria (s/nilable :fhir/Expression)) + :ret (s/or :expression-name string? :anomaly ::anom/anomaly)) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj index 67591897e..2619183f7 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/measure/util_test.clj @@ -17,23 +17,25 @@ (deftest expression-test (testing "missing criteria" - (given (u/expression (constantly "path-184642") nil) + (given (u/expression-name (constantly "path-184642") nil) ::anom/category := ::anom/incorrect ::anom/message := "Missing criteria." :fhir/issue := "required" :fhir.issue/expression := "path-184642")) (testing "unsupported language" - (given (u/expression (constantly "path-184706") - {:language #fhir/code"lang-184851"}) + (given (u/expression-name (constantly "path-184706") + {:fhir/type :fhir/Expression + :language #fhir/code"lang-184851"}) ::anom/category := ::anom/unsupported ::anom/message := "Unsupported language `lang-184851`." :fhir/issue := "not-supported" :fhir.issue/expression := "path-184706.criteria.language")) (testing "missing expression" - (given (u/expression (constantly "path-184642") - {:language #fhir/code"text/cql-identifier"}) + (given (u/expression-name (constantly "path-184642") + {:fhir/type :fhir/Expression + :language #fhir/code"text/cql-identifier"}) ::anom/category := ::anom/incorrect ::anom/message := "Missing expression." :fhir/issue := "required" @@ -42,13 +44,15 @@ (testing "works with `text/cql-identifier`" (satisfies-prop 10 (prop/for-all [expression gen/string] - (= expression (u/expression (constantly "foo") - {:language #fhir/code"text/cql-identifier" - :expression expression}))))) + (= expression (u/expression-name (constantly "foo") + {:fhir/type :fhir/Expression + :language #fhir/code"text/cql-identifier" + :expression expression}))))) (testing "works with `text/cql`" (satisfies-prop 10 (prop/for-all [expression gen/string] - (= expression (u/expression (constantly "foo") - {:language #fhir/code"text/cql" - :expression expression})))))) + (= expression (u/expression-name (constantly "foo") + {:fhir/type :fhir/Expression + :language #fhir/code"text/cql" + :expression expression})))))) 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 f311ed0fc..a8371991c 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 @@ -1,10 +1,10 @@ (ns blaze.fhir.operation.evaluate-measure.measure-test (:require - [blaze.anomaly :as ba] [blaze.db.api :as d] [blaze.db.api-stub :refer [mem-node-config with-system-data]] [blaze.fhir.operation.evaluate-measure.measure :as measure] [blaze.fhir.operation.evaluate-measure.measure-spec] + [blaze.fhir.operation.evaluate-measure.measure.group-spec] [blaze.fhir.operation.evaluate-measure.measure.population-spec] [blaze.fhir.operation.evaluate-measure.measure.stratifier-spec] [blaze.fhir.operation.evaluate-measure.measure.util-spec] @@ -91,7 +91,7 @@ :blaze/base-url "" ::reitit/router router :executor executor} measure @(d/pull node (d/resource-handle db "Measure" "0")) - period [#system/date"2000" #system/date"2020"]] + period [#system/date "2000" #system/date "2020"]] (try @(measure/evaluate-measure context measure {:period period :report-type report-type}) @@ -106,14 +106,16 @@ :group first :population first))) -(defn- first-stratifier-strata [result] +(defn- first-stratifier [result] (if (::anom/category result) (prn result) (-> result :resource :group first - :stratifier first - :stratum))) + :stratifier first))) + +(defn- first-stratifier-strata [result] + (:stratum (first-stratifier result))) (defn- population-concept [code] (type/codeable-concept @@ -215,7 +217,7 @@ :criteria (cql-expression "InInitialPopulation")}]}]}] (testing "population report" - (let [params {:period [#system/date"2000" #system/date"2100"] + (let [params {:period [#system/date "2000" #system/date "2100"] :report-type "population"}] (given (:resource @(measure/evaluate-measure context measure params)) :fhir/type := :fhir/MeasureReport @@ -223,7 +225,7 @@ [:group 0 :population 0 :count] := 3))) (testing "subject-list report" - (let [params {:period [#system/date"2000" #system/date"2100"] + (let [params {:period [#system/date "2000" #system/date "2100"] :report-type "subject-list"} {:keys [resource tx-ops]} @(measure/evaluate-measure context measure params)] @@ -263,6 +265,7 @@ :library [#fhir/canonical"0"] :group [{:fhir/type :fhir.Measure/group + :code #fhir/CodeableConcept{:text #fhir/string"group-1"} :extension [#fhir/Extension {:url "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis" @@ -276,6 +279,7 @@ :code #fhir/CodeableConcept{:text #fhir/string"gender"} :criteria (cql-expression "Gender")}]} {:fhir/type :fhir.Measure/group + :code #fhir/CodeableConcept{:text #fhir/string"group-2"} :extension [#fhir/Extension {:url "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis" @@ -286,35 +290,40 @@ :criteria (cql-expression "AllEncounters")}]}]}] (testing "population report" - (let [params {:period [#system/date"2000" #system/date"2100"] + (let [params {:period [#system/date "2000" #system/date "2100"] :report-type "population"}] (given (:resource @(measure/evaluate-measure context measure params)) :fhir/type := :fhir/MeasureReport + [:group count] := 2 + [:group 0 :code] := #fhir/CodeableConcept{:text #fhir/string"group-1"} [:group 0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" [:group 0 :population 0 :count] := 4 + [:group 1 :code] := #fhir/CodeableConcept{:text #fhir/string"group-2"} [:group 1 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" [:group 1 :population 0 :count] := 6))) (testing "subject-list report" - (let [params {:period [#system/date"2000" #system/date"2100"] + (let [params {:period [#system/date "2000" #system/date "2100"] :report-type "subject-list"} {:keys [resource tx-ops]} @(measure/evaluate-measure context measure params)] (given resource :fhir/type := :fhir/MeasureReport [:group count] := 2 + [:group 0 :code] := #fhir/CodeableConcept{:text #fhir/string"group-1"} [:group 0 :population count] := 1 [:group 0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" [:group 0 :population 0 :count] := 4 [:group 0 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAA" [:group 0 :stratifier count] := 1 [:group 0 :stratifier 0 :stratum count] := 2 - [:group 0 :stratifier 0 :stratum 0 :value :text] := "female" - [:group 0 :stratifier 0 :stratum 0 :population 0 :count] := 3 - [:group 0 :stratifier 0 :stratum 0 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAC" - [:group 0 :stratifier 0 :stratum 1 :value :text] := "male" - [:group 0 :stratifier 0 :stratum 1 :population 0 :count] := 1 - [:group 0 :stratifier 0 :stratum 1 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAB" + [:group 0 :stratifier 0 :stratum 0 :value :text] := "male" + [:group 0 :stratifier 0 :stratum 0 :population 0 :count] := 1 + [:group 0 :stratifier 0 :stratum 0 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAB" + [:group 0 :stratifier 0 :stratum 1 :value :text] := "female" + [:group 0 :stratifier 0 :stratum 1 :population 0 :count] := 3 + [:group 0 :stratifier 0 :stratum 1 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAC" + [:group 1 :code] := #fhir/CodeableConcept{:text #fhir/string"group-2"} [:group 1 :population count] := 1 [:group 1 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" [:group 1 :population 0 :count] := 6 @@ -368,8 +377,9 @@ :population [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "population"}] + (given-failed-future (measure/evaluate-measure context measure params) ::anom/category := ::anom/incorrect ::anom/message := "Syntax error at " @@ -395,8 +405,9 @@ :population [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "population"}] + (given-failed-future (measure/evaluate-measure context measure params) ::anom/category := ::anom/incorrect ::anom/message := "Missing criteria." @@ -425,12 +436,14 @@ [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population") :criteria (cql-expression "InInitialPopulation")}]}]} - params {:period [#system/date"2000" #system/date"2020"] - :report-type "population"}] - (given-failed-future (measure/evaluate-measure context measure params) - ::anom/category := ::anom/interrupted - ::anom/message := "Timeout of 0 millis eclipsed while evaluating." - :measure-id := measure-id)))) + params {:period [#system/date "2000" #system/date "2020"]}] + + (doseq [report-type ["population" "subject-list"] + :let [params (assoc params :report-type report-type)]] + (given-failed-future (measure/evaluate-measure context measure params) + ::anom/category := ::anom/interrupted + ::anom/message := "Timeout of 0 millis eclipsed while evaluating." + :measure-id := measure-id))))) (testing "single subject" (doseq [subject-ref ["0" ["Patient" "0"]] @@ -454,18 +467,19 @@ [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population") :criteria (cql-expression "InInitialPopulation")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "subject" :subject-ref subject-ref}] + (given (:resource @(measure/evaluate-measure context measure params)) :fhir/type := :fhir/MeasureReport :status := #fhir/code"complete" :type := #fhir/code"individual" :measure := #fhir/canonical"measure-155437" [:subject :reference] := "Patient/0" - :date := #system/date-time"1970-01-01T00:00Z" - :period := #fhir/Period{:start #system/date-time"2000" - :end #system/date-time"2020"} + :date := #system/date-time "1970-01-01T00:00Z" + :period := #fhir/Period{:start #system/date-time "2000" + :end #system/date-time "2020"} [:group 0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" [:group 0 :population 0 :count] := count)))) @@ -494,22 +508,23 @@ [{:fhir/type :fhir.Measure.group/stratifier :code #fhir/CodeableConcept{:text #fhir/string"gender"} :criteria (cql-expression "Gender")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "subject" :subject-ref "0"}] + (given (:resource @(measure/evaluate-measure context measure params)) :fhir/type := :fhir/MeasureReport :status := #fhir/code"complete" :type := #fhir/code"individual" :measure := #fhir/canonical"measure-155502" [:subject :reference] := "Patient/0" - :date := #system/date-time"1970-01-01T00:00Z" - :period := #fhir/Period{:start #system/date-time"2000" - :end #system/date-time"2020"} + :date := #system/date-time "1970-01-01T00:00Z" + :period := #fhir/Period{:start #system/date-time "2000" + :end #system/date-time "2020"} [:group 0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" [:group 0 :population 0 :count] := count - [:group 0 :stratifier 0 :code 0 :text type/value] := "gender" - [:group 0 :stratifier 0 :stratum 0 :value :text type/value] := (when (= 1 count) "male") + [:group 0 :stratifier 0 :code 0 :text] := "gender" + [:group 0 :stratifier 0 :stratum 0 :value :text] := (when (= 1 count) "male") [:group 0 :stratifier 0 :stratum 0 :population 0 :count] := (when (= 1 count) 1)))))) (testing "invalid subject" @@ -530,9 +545,10 @@ [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population") :criteria (cql-expression "InInitialPopulation")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "subject" :subject-ref ["Observation" "0"]}] + (given-failed-future (measure/evaluate-measure context measure params) ::anom/category := ::anom/incorrect ::anom/message := "Type mismatch between evaluation subject `Observation` and Measure subject `Patient`.")))) @@ -555,9 +571,10 @@ [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population") :criteria (cql-expression "InInitialPopulation")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "subject" :subject-ref "0"}] + (given-failed-future (measure/evaluate-measure context measure params) ::anom/category := ::anom/incorrect ::anom/message := "Subject with type `Patient` and id `0` was not found.")))) @@ -582,9 +599,10 @@ [{:fhir/type :fhir.Measure.group/population :code (population-concept "initial-population") :criteria (cql-expression "InInitialPopulation")}]}]} - params {:period [#system/date"2000" #system/date"2020"] + params {:period [#system/date "2000" #system/date "2020"] :report-type "subject" :subject-ref "0"}] + (given-failed-future (measure/evaluate-measure context measure params) ::anom/category := ::anom/incorrect ::anom/message := "Subject with type `Patient` and id `0` was not found.")))))) @@ -682,8 +700,9 @@ :fhir/type := :fhir/List :id := "AAAAAAAAAAAAAAAA" [:entry 0 :item :reference] := "Patient/0" - [:entry 1 :item :reference] := "Patient/3")) + [:entry 1 :item :reference] := "Patient/3"))) +(deftest stratifier-integration-test (let [result (evaluate "q19-stratifier-ageclass")] (testing "MeasureReport is valid" (is (s/valid? :blaze/resource (:resource result)))) @@ -691,13 +710,19 @@ (testing "MeasureReport type is `summary`" (is (= #fhir/code"summary" (-> result :resource :type)))) + (given (first-stratifier result) + keys := [:fhir/type :code :stratum] + :fhir/type := :fhir.MeasureReport.group/stratifier + [:code count] := #fhir/integer 1 + [:code 0] := #fhir/CodeableConcept{:text "age-class"}) + (given (first-stratifier-strata result) - [0 :value :text type/value] := "10" + [0 :value :text] := "10" [0 :population 0 :code :coding 0 :system] := #fhir/uri"http://terminology.hl7.org/CodeSystem/measure-population" [0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" - [0 :population 0 :count] := 1 - [1 :value :text type/value] := "70" - [1 :population 0 :count] := 2)) + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "70" + [1 :population 0 :count] := #fhir/integer 2)) (let [result (evaluate "q19-stratifier-ageclass" "subject-list")] (testing "MeasureReport is valid" @@ -707,11 +732,11 @@ (is (= #fhir/code"subject-list" (-> result :resource :type)))) (given (first-stratifier-strata result) - [0 :value :text type/value] := "10" - [0 :population 0 :count] := 1 + [0 :value :text] := "10" + [0 :population 0 :count] := #fhir/integer 1 [0 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAB" - [1 :value :text type/value] := "70" - [1 :population 0 :count] := 2 + [1 :value :text] := "70" + [1 :population 0 :count] := #fhir/integer 2 [1 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAC") (given (:tx-ops result) @@ -726,53 +751,64 @@ [2 1 :entry 1 :item :reference] := "Patient/2")) (given (first-stratifier-strata (evaluate "q20-stratifier-city")) - [0 :value :text type/value] := "Jena" - [0 :population 0 :count] := 3 - [1 :value :text type/value] := "Leipzig" - [1 :population 0 :count] := 1) + [0 :value :text] := "Jena" + [0 :population 0 :count] := #fhir/integer 3 + [1 :value :text] := "Leipzig" + [1 :population 0 :count] := #fhir/integer 1) (given (first-stratifier-strata (evaluate "q21-stratifier-city-of-only-women")) - [0 :value :text type/value] := "Jena" - [0 :population 0 :count] := 2) - - (is (ba/incorrect? (evaluate "q22-stratifier-multiple-cities-fail"))) - - (given (first-stratifier-strata (evaluate "q23-stratifier-ageclass-and-gender")) - [0 :component 0 :code :text type/value] := "age-class" - [0 :component 0 :value :text type/value] := "10" - [0 :component 1 :code :text type/value] := "gender" - [0 :component 1 :value :text type/value] := "male" - [0 :population 0 :code :coding 0 :system] := #fhir/uri"http://terminology.hl7.org/CodeSystem/measure-population" - [0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" - [0 :population 0 :count] := 1 - [1 :component 0 :value :text type/value] := "70" - [1 :component 1 :value :text type/value] := "female" - [1 :population 0 :count] := 2 - [2 :component 0 :value :text type/value] := "70" - [2 :component 1 :value :text type/value] := "male" - [2 :population 0 :count] := 1) + [0 :value :text] := "Jena" + [0 :population 0 :count] := #fhir/integer 2) + + (given (evaluate "q22-stratifier-multiple-cities-fail") + ::anom/category := ::anom/incorrect + ::anom/message := "CQL expression `City` returned more than one value for resource `Patient/0`.") + + (let [result (evaluate "q23-stratifier-ageclass-and-gender")] + (given (first-stratifier result) + keys := [:fhir/type :stratum] + :fhir/type := :fhir.MeasureReport.group/stratifier) + + (given (first-stratifier-strata result) + count := 3 + [0 keys] := [:fhir/type :component :population] + [0 :fhir/type] := :fhir.MeasureReport.group.stratifier/stratum + [0 :component 0 :code :text] := "age-class" + [0 :component 0 :value :text] := "10" + [0 :component 1 :code :text] := "gender" + [0 :component 1 :value :text] := "male" + [0 :population 0 :code :coding 0 :system] := #fhir/uri"http://terminology.hl7.org/CodeSystem/measure-population" + [0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" + [0 :population 0 :count] := #fhir/integer 1 + [1 :component 0 :value :text] := "70" + [1 :component 1 :value :text] := "male" + [1 :population 0 :count] := #fhir/integer 1 + [2 :component 0 :value :text] := "70" + [2 :component 1 :value :text] := "female" + [2 :population 0 :count] := #fhir/integer 2)) (let [result (evaluate "q23-stratifier-ageclass-and-gender" "subject-list")] (testing "MeasureReport is valid" (is (s/valid? :blaze/resource (:resource result)))) (given (first-stratifier-strata result) - [0 :component 0 :code :text type/value] := "age-class" - [0 :component 0 :value :text type/value] := "10" - [0 :component 1 :code :text type/value] := "gender" - [0 :component 1 :value :text type/value] := "male" + count := 3 + [0 :component 0 :code :text] := "age-class" + [0 :component 0 :value :text] := "10" + [0 :component 1 :code :text] := "gender" + [0 :component 1 :value :text] := "male" [0 :population 0 :code :coding 0 :system] := #fhir/uri"http://terminology.hl7.org/CodeSystem/measure-population" [0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" - [0 :population 0 :count] := 1 + [0 :population 0 :count] := #fhir/integer 1 [0 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAB" - [1 :component 0 :value :text type/value] := "70" - [1 :component 1 :value :text type/value] := "female" - [1 :population 0 :count] := 2 - [1 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAD" - [2 :component 0 :value :text type/value] := "70" - [2 :component 1 :value :text type/value] := "male" - [2 :population 0 :count] := 1 - [2 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAC") + [1 :component 0 :value :text] := "70" + [1 :component 1 :value :text] := "male" + [1 :population 0 :count] := #fhir/integer 1 + [1 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAC" + [2 :component 0 :value :text] := "70" + [2 :component 1 :value :text] := "female" + [2 :population 0 :count] := #fhir/integer 2 + [2 :population 0 :subjectResults :reference] := "List/AAAAAAAAAAAAAAAD") (given (:tx-ops result) [1 1 :fhir/type] := :fhir/List @@ -787,78 +823,124 @@ [3 1 :entry 1 :item :reference] := "Patient/3")) (given (first-stratifier-strata (evaluate "q25-stratifier-collection")) - [0 :value :text type/value] := "Organization/collection-0" - [0 :population 0 :count] := 1 - [1 :value :text type/value] := "Organization/collection-1" - [1 :population 0 :count] := 1) + [0 :value :text] := "Organization/collection-0" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "Organization/collection-1" + [1 :population 0 :count] := #fhir/integer 1) (given (first-stratifier-strata (evaluate "q26-stratifier-bmi")) - [0 :value :text type/value] := "37" - [0 :population 0 :count] := 1 - [1 :value :text type/value] := "null" - [1 :population 0 :count] := 2) + [0 :value :text] := "37" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "null" + [1 :population 0 :count] := #fhir/integer 2) (given (first-stratifier-strata (evaluate "q27-stratifier-calculated-bmi")) - [0 :value :text type/value] := "26.8" - [0 :population 0 :count] := 1 - [1 :value :text type/value] := "null" - [1 :population 0 :count] := 2) + [0 :value :text] := "26.8" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "null" + [1 :population 0 :count] := #fhir/integer 2) (given (first-stratifier-strata (evaluate "q29-stratifier-sample-material-type")) count := 2 - [0 :value :text type/value] := "liquid" - [0 :population 0 :count] := 1 - [1 :value :text type/value] := "tissue" - [1 :population 0 :count] := 1) + [0 :value :text] := "liquid" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "tissue" + [1 :population 0 :count] := #fhir/integer 1) (given (evaluate "q30-stratifier-with-missing-expression") - ::anom/category := ::anom/incorrect, - ::anom/message := "Missing expression with name `SampleMaterialTypeCategory`.", + ::anom/category := ::anom/incorrect + + ::anom/message := "Missing expression with name `SampleMaterialTypeCategory`." + :expression-name := "SampleMaterialTypeCategory" :measure-id := "0") (given (first-stratifier-strata (evaluate "q31-stratifier-storage-temperature")) count := 2 - [0 :value :text type/value] := "temperature2to10" - [0 :population 0 :count] := 1 - [1 :value :text type/value] := "temperatureGN" - [1 :population 0 :count] := 1) + [0 :value :text] := "temperature2to10" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "temperatureGN" + [1 :population 0 :count] := #fhir/integer 1) (given (first-stratifier-strata (evaluate "q32-stratifier-underweight")) count := 2 - [0 :value :text type/value] := "false" - [0 :population 0 :count] := 2 - [1 :value :text type/value] := "true" - [1 :population 0 :count] := 1) + [0 :value :text] := "true" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :text] := "false" + [1 :population 0 :count] := #fhir/integer 2) (given (first-stratifier-strata (evaluate "q40-specimen-stratifier")) count := 2 - [0 :value :text type/value] := "blood-plasma" - [0 :population 0 :count] := 4 - [1 :value :text type/value] := "peripheral-blood-cells-vital" - [1 :population 0 :count] := 3) + [0 :value :text] := "blood-plasma" + [0 :population 0 :count] := #fhir/integer 4 + [1 :value :text] := "peripheral-blood-cells-vital" + [1 :population 0 :count] := #fhir/integer 3) (given (first-stratifier-strata (evaluate "q41-specimen-multi-stratifier")) count := 4 [0 :component 0 :code :coding 0 :code type/value] := "sample-diagnosis" - [0 :component 0 :value :text type/value] := "C34.9" + [0 :component 0 :value :text] := "C34.9" [0 :component 1 :code :coding 0 :code type/value] := "sample-type" - [0 :component 1 :value :text type/value] := "blood-plasma" - [0 :population 0 :count] := 2 - [1 :component 0 :value :text type/value] := "C34.9" - [1 :component 1 :value :text type/value] := "peripheral-blood-cells-vital" - [1 :population 0 :count] := 1 - [2 :component 0 :value :text type/value] := "C50.9" - [2 :component 1 :value :text type/value] := "blood-plasma" - [2 :population 0 :count] := 2 - [3 :component 0 :value :text type/value] := "C50.9" - [3 :component 1 :value :text type/value] := "peripheral-blood-cells-vital" - [3 :population 0 :count] := 2) + [0 :component 1 :value :text] := "blood-plasma" + [0 :population 0 :count] := #fhir/integer 2 + [1 :component 0 :value :text] := "C34.9" + [1 :component 1 :value :text] := "peripheral-blood-cells-vital" + [1 :population 0 :count] := #fhir/integer 1 + [2 :component 0 :value :text] := "C50.9" + [2 :component 1 :value :text] := "blood-plasma" + [2 :population 0 :count] := #fhir/integer 2 + [3 :component 0 :value :text] := "C50.9" + [3 :component 1 :value :text] := "peripheral-blood-cells-vital" + [3 :population 0 :count] := #fhir/integer 2) (given (first-stratifier-strata (evaluate "q52-sort-with-missing-values")) count := 1 - [0 :value :text type/value] := "Condition[id = 0, t = 1]" - [0 :population 0 :count] := 1)) + [0 :value :text] := "Condition[id = 0, t = 1]" + [0 :population 0 :count] := #fhir/integer 1) + + (given (first-stratifier-strata (evaluate "q54-stratifier-condition-code")) + count := 2 + [0 :value :coding 0 :system] := #fhir/uri"http://hl7.org/fhir/sid/icd-10" + [0 :value :coding 0 :code] := #fhir/code"C41.9" + [0 :population 0 :count] := #fhir/integer 1 + [1 :value :coding 0 :system] := #fhir/uri"http://hl7.org/fhir/sid/icd-10" + [1 :value :coding 0 :code] := #fhir/code"C41.6" + [1 :population 0 :count] := #fhir/integer 2) + + (given (first-stratifier-strata (evaluate "q55-stratifier-bmi-observation")) + count := 1 + [0 :value :text] := "36.6 kg/m2" + [0 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.value" + [0 :extension 0 :value :value] := 36.6M + [0 :extension 0 :value :unit] := "kg/m2" + [0 :extension 0 :value :system] := #fhir/uri"http://unitsofmeasure.org" + [0 :extension 0 :value :code] := #fhir/code"kg/m2" + [0 :population 0 :count] := #fhir/integer 1) + + (given (first-stratifier-strata (evaluate "q56-stratifier-observation-code-value")) + count := 2 + [0 :component 0 :code :text] := "code" + [0 :component 0 :value :coding 0 :system] := #fhir/uri"http://loinc.org" + [0 :component 0 :value :coding 0 :code] := #fhir/code"39156-5" + [0 :component 1 :code :text] := "value" + [0 :component 1 :value :text] := "36.6 kg/m2" + [0 :component 1 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.component.value" + [0 :component 1 :extension 0 :value :value] := 36.6M + [0 :component 1 :extension 0 :value :unit] := "kg/m2" + [0 :component 1 :extension 0 :value :system] := #fhir/uri"http://unitsofmeasure.org" + [0 :component 1 :extension 0 :value :code] := #fhir/code"kg/m2" + [0 :population 0 :count] := #fhir/integer 1 + [1 :component 1 :extension 0 :url] := "http://hl7.org/fhir/5.0/StructureDefinition/extension-MeasureReport.group.stratifier.stratum.component.value" + [1 :component 0 :code :text] := "code" + [1 :component 0 :value :coding 0 :system] := #fhir/uri"http://loinc.org" + [1 :component 0 :value :coding 0 :code] := #fhir/code"8302-2" + [1 :component 1 :code :text] := "value" + [1 :component 1 :value :text] := "178 cm" + [1 :component 1 :extension 0 :value :value] := 178M + [1 :component 1 :extension 0 :value :unit] := "cm" + [1 :component 1 :extension 0 :value :system] := #fhir/uri"http://unitsofmeasure.org" + [1 :component 1 :extension 0 :value :code] := #fhir/code"cm" + [1 :population 0 :count] := #fhir/integer 1)) (comment (log/set-level! :debug) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.cql new file mode 100644 index 000000000..3a227e08e --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.cql @@ -0,0 +1,11 @@ +library "q54-stratifier-condition-code" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +context Patient + +define InInitialPopulation: + [Condition] + +define function Code(condition FHIR.Condition): + condition.code diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.json b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.json new file mode 100644 index 000000000..786eca6d1 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q54-stratifier-condition-code.json @@ -0,0 +1,157 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "0" + }, + "request": { + "method": "PUT", + "url": "Patient/0" + } + }, + { + "resource": { + "resourceType": "Patient", + "id": "1" + }, + "request": { + "method": "PUT", + "url": "Patient/1" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "0", + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/sid/icd-10", + "code": "C41.9" + } + ] + }, + "subject": { + "reference": "Patient/0" + } + }, + "request": { + "method": "PUT", + "url": "Condition/0" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "1", + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/sid/icd-10", + "code": "C41.6" + } + ] + }, + "subject": { + "reference": "Patient/1" + } + }, + "request": { + "method": "PUT", + "url": "Condition/1" + } + }, + { + "resource": { + "resourceType": "Condition", + "id": "2", + "code": { + "coding": [ + { + "system": "http://hl7.org/fhir/sid/icd-10", + "code": "C41.6" + } + ] + }, + "subject": { + "reference": "Patient/0" + } + }, + "request": { + "method": "PUT", + "url": "Condition/2" + } + }, + { + "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": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Condition" + } + ], + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "InInitialPopulation" + } + } + ], + "stratifier": [ + { + "code": { + "text": "code" + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Code" + } + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Measure/0" + } + } + ] +} diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.cql new file mode 100644 index 000000000..3db025bc3 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.cql @@ -0,0 +1,13 @@ +library "q55-stratifier-bmi-observation" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +codesystem loinc: 'http://loinc.org' + +context Patient + +define InInitialPopulation: + [Observation: Code '39156-5' from loinc] + +define function Bmi(observation FHIR.Observation): + observation.value diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.json b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.json new file mode 100644 index 000000000..b2702c1d3 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q55-stratifier-bmi-observation.json @@ -0,0 +1,111 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "0" + }, + "request": { + "method": "PUT", + "url": "Patient/0" + } + }, + { + "resource": { + "resourceType": "Observation", + "id": "0", + "code": { + "coding": [ + { + "system": "http://loinc.org", + "code": "39156-5" + } + ] + }, + "subject": { + "reference": "Patient/0" + }, + "valueQuantity": { + "value": 36.6, + "unit": "kg/m2", + "system": "http://unitsofmeasure.org", + "code": "kg/m2" + } + }, + "request": { + "method": "PUT", + "url": "Observation/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": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Observation" + } + ], + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "InInitialPopulation" + } + } + ], + "stratifier": [ + { + "code": { + "text": "bmi" + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Bmi" + } + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Measure/0" + } + } + ] +} diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.cql b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.cql new file mode 100644 index 000000000..b5ce4a283 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.cql @@ -0,0 +1,16 @@ +library "q56-stratifier-observation-code-value" +using FHIR version '4.0.0' +include FHIRHelpers version '4.0.0' + +codesystem loinc: 'http://loinc.org' + +context Patient + +define InInitialPopulation: + [Observation] + +define function Code(observation FHIR.Observation): + observation.code + +define function Value(observation FHIR.Observation): + observation.value diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.json b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.json new file mode 100644 index 000000000..5d8a0e728 --- /dev/null +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q56-stratifier-observation-code-value.json @@ -0,0 +1,154 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "0" + }, + "request": { + "method": "PUT", + "url": "Patient/0" + } + }, + { + "resource": { + "resourceType": "Observation", + "id": "0", + "code": { + "coding": [ + { + "system": "http://loinc.org", + "code": "39156-5" + } + ] + }, + "subject": { + "reference": "Patient/0" + }, + "valueQuantity": { + "value": 36.6, + "unit": "kg/m2", + "system": "http://unitsofmeasure.org", + "code": "kg/m2" + } + }, + "request": { + "method": "PUT", + "url": "Observation/0" + } + }, + { + "resource": { + "resourceType": "Observation", + "id": "1", + "code": { + "coding": [ + { + "system": "http://loinc.org", + "code": "8302-2" + } + ] + }, + "subject": { + "reference": "Patient/0" + }, + "valueQuantity": { + "value": 178, + "unit": "cm", + "system": "http://unitsofmeasure.org", + "code": "cm" + } + }, + "request": { + "method": "PUT", + "url": "Observation/1" + } + }, + { + "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": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Observation" + } + ], + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "InInitialPopulation" + } + } + ], + "stratifier": [ + { + "code": { + "text": "code-value" + }, + "component": [ + { + "code": { + "text": "code" + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Code" + } + }, + { + "code": { + "text": "value" + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Value" + } + } + ] + } + ] + } + ] + }, + "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 78abbb0a7..65add7e63 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 @@ -521,12 +521,12 @@ [:group 0 :stratifier 0 :code 0 :text] := #fhir/string"gender" [:group 0 :stratifier 0 :stratum 0 :population 0 :code :coding 0 :system] := measure-population-uri [:group 0 :stratifier 0 :stratum 0 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" - [:group 0 :stratifier 0 :stratum 0 :population 0 :count] := 2 - [:group 0 :stratifier 0 :stratum 0 :value :text] := #fhir/string"female" + [:group 0 :stratifier 0 :stratum 0 :population 0 :count] := 1 + [:group 0 :stratifier 0 :stratum 0 :value :text] := #fhir/string"male" [:group 0 :stratifier 0 :stratum 1 :population 0 :code :coding 0 :system] := measure-population-uri [:group 0 :stratifier 0 :stratum 1 :population 0 :code :coding 0 :code] := #fhir/code"initial-population" - [:group 0 :stratifier 0 :stratum 1 :population 0 :count] := 1 - [:group 0 :stratifier 0 :stratum 1 :value :text] := #fhir/string"male"))))) + [:group 0 :stratifier 0 :stratum 1 :population 0 :count] := 2 + [:group 0 :stratifier 0 :stratum 1 :value :text] := #fhir/string"female"))))) (testing "as POST request" (testing "with no Prefer header"