From 27a5569a84aa3c2f1ab7f5aa9e39ab68028e24c2 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Sat, 1 Jul 2023 15:56:10 +0400 Subject: [PATCH 01/12] Add test Ref #21 --- tests/test_real.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/test_real.py b/tests/test_real.py index 8025efa..d7fb4da 100644 --- a/tests/test_real.py +++ b/tests/test_real.py @@ -95,5 +95,37 @@ def pickle_test(): assert reload(resource) == ["Patient/cdf"] - - +def extension_test(): + patient = { + "identifier": [ + { + "period": { + "start": "2020-01-01" + }, + "system": "http://hl7.org/fhir/sid/us-mbi", + "type": { + "coding": [ + { + "code": "MC", + "display": "Patient's Medicare number", + "extension": [ + { + "url": "https://bluebutton.cms.gov/resources/codesystem/identifier-currency", + "valueCoding": { + "code": "current", + "display": "Current", + "system": "https://bluebutton.cms.gov/resources/codesystem/identifier-currency" + } + } + ], + "system": "http://terminology.hl7.org/CodeSystem/v2-0203" + } + ] + }, + "value": "7SM0A00AA00" + } + ], + "resourceType": "Patient" + } + result = evaluate(patient, "Patient.identifier.where(type.coding.extension('https://bluebutton.cms.gov/resources/codesystem/identifier-currency').valueCoding.code = 'current').where(system = 'http://hl7.org/fhir/sid/us-mbi').value") + assert result == ["7SM0A00AA00"] From 7de0ad4c81e7bcfc52eb1a8d36b1db4c39b18bbc Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Tue, 4 Jul 2023 14:55:47 +0400 Subject: [PATCH 02/12] Simple extension implementation --- fhirpathpy/engine/invocations/__init__.py | 1 + fhirpathpy/engine/invocations/filtering.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/fhirpathpy/engine/invocations/__init__.py b/fhirpathpy/engine/invocations/__init__.py index 04a5f0f..b8f9f9d 100644 --- a/fhirpathpy/engine/invocations/__init__.py +++ b/fhirpathpy/engine/invocations/__init__.py @@ -27,6 +27,7 @@ "count": {"fn": existence.count_fn}, "repeat": {"fn": filtering.repeat_macro, "arity": {1: ["Expr"]}}, "where": {"fn": filtering.where_macro, "arity": {1: ["Expr"]}}, + "extension": {"fn": filtering.extension, "arity": {1: ["String"]}}, "select": {"fn": filtering.select_macro, "arity": {1: ["Expr"]}}, "single": {"fn": filtering.single_fn}, "first": {"fn": filtering.first_fn}, diff --git a/fhirpathpy/engine/invocations/filtering.py b/fhirpathpy/engine/invocations/filtering.py index 5e8a490..7a8b2b0 100644 --- a/fhirpathpy/engine/invocations/filtering.py +++ b/fhirpathpy/engine/invocations/filtering.py @@ -115,3 +115,11 @@ def check_fhir_type(ctx, x, tp): def of_type_fn(ctx, coll, tp): return list(filter(lambda x: check_fhir_type(ctx, util.get_data(x), tp), coll)) + +def extension(ctx,data,url): + res = [] + for d in data: + exts = [e for e in util.get_data(d)["extension"] if e["url"] == url] + if len(exts) > 0: + res.append(exts[0]) + return res From c51b41b70199696cfd7b9392e8fdbbf9ef3f75a9 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Tue, 4 Jul 2023 14:56:34 +0400 Subject: [PATCH 03/12] Add extensions data tests --- tests/cases/extensions.yaml | 108 +++++++++++++++++++++++++ tests/resources/patient-example-2.json | 26 ++++++ 2 files changed, 134 insertions(+) create mode 100644 tests/cases/extensions.yaml create mode 100644 tests/resources/patient-example-2.json diff --git a/tests/cases/extensions.yaml b/tests/cases/extensions.yaml new file mode 100644 index 0000000..e309c79 --- /dev/null +++ b/tests/cases/extensions.yaml @@ -0,0 +1,108 @@ +tests: + # https://www.hl7.org/fhir/fhirpath.html#types + - 'group: Extension and id for primitive types': + + - desc: '** id for primitive type' + expression: Functions.attrtrue.id = 'someid' + result: + - true + + - desc: '** expression with extension for primitive type 1' + inputfile: patient-example.json + expression: Patient.birthDate.extension.where(url = '').empty() + result: + - true + + - desc: '** expression with extension for primitive type 2' + inputfile: patient-example.json + expression: >- + Patient.birthDate.extension + .where(url = 'http://hl7.org/fhir/StructureDefinition/patient-birthTime') + .valueDateTime.toDateTime() = @1974-12-25T14:35:45-05:00 + result: + - true + + - desc: '** expression with extension for primitive type 3' + inputfile: patient-example.json + model: r4 + expression: >- + Patient.birthDate.extension + .where(url = 'http://hl7.org/fhir/StructureDefinition/patient-birthTime') + .value = @1974-12-25T14:35:45-05:00 + result: + - true + + # https://www.hl7.org/fhir/fhirpath.html#functions + - 'group: Additional functions': + - desc: 'extension(url : string) : collection' + + # If the url is empty ({ }), the result is empty. + - desc: '** empty url' + inputfile: patient-example.json + expression: Patient.birthDate.extension('').empty() + result: + - true + + # If the input collection is empty ({ }), the result is empty. + - desc: '** empty input collection' + inputfile: patient-example.json + expression: >- + Patient.birthDate1 + .extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime').empty() + result: + - true + + - desc: '** expression with extension() for primitive type (without using FHIR model data)' + inputfile: patient-example.json + expression: >- + Patient.birthDate.extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime') + .valueDateTime.toDateTime() = @1974-12-25T14:35:45-05:00 + result: + - true + + - desc: '** expression with extension() for primitive type (without using FHIR model data) when only extension is present' + inputfile: patient-example-2.json + expression: >- + Patient.communication.preferred.extension('test').exists() + result: + - true + + - desc: '** expression with extension() for primitive type (using FHIR model data) when only extension is present' + inputfile: patient-example-2.json + model: r4 + expression: >- + Patient.communication.preferred.extension('test').value.id + result: + - testing + + - desc: '** expression with extension() for primitive type (using FHIR model data)' + inputfile: patient-example.json + model: r4 + expression: >- + Patient.birthDate.extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime') + .value = @1974-12-25T14:35:45-05:00 + result: + - true + + - desc: '** value of extension of extension (using FHIR model data)' + model: r4 + expression: Functions.attrtrue.extension('url1').extension('url2').value = 'someuri' + result: + - true + + - desc: '** id of extension of extension' + expression: Functions.attrtrue.extension('url1').extension('url2').id = 'someid2' + result: + - true + +subject: + resourceType: Functions + attrtrue: true + _attrtrue: + id: someid + extension: + - url: url1 + extension: + - url: url2 + id: someid2 + valueUri: someuri diff --git a/tests/resources/patient-example-2.json b/tests/resources/patient-example-2.json new file mode 100644 index 0000000..1947ac1 --- /dev/null +++ b/tests/resources/patient-example-2.json @@ -0,0 +1,26 @@ +{ + "resourceType": "Patient", + "communication": [ + { + "language": { + "coding": [ + { + "system": "urn:ietf:bcp:47", + "code": "nl", + "display": "Dutch" + } + ] + }, + "_preferred": { + "extension": [ + { + "url": "test", + "_valueString": { + "id": "testing" + } + } + ] + } + } + ] +} From 3867c75d0dd5717b0f1bfbc41968f2821a355862 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 15:59:42 +0400 Subject: [PATCH 04/12] Basic support for primitive types --- fhirpathpy/engine/evaluators/__init__.py | 19 ++++++++++++------- fhirpathpy/engine/invocations/filtering.py | 13 ++++++++----- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/fhirpathpy/engine/evaluators/__init__.py b/fhirpathpy/engine/evaluators/__init__.py index f73db6b..6fe725c 100644 --- a/fhirpathpy/engine/evaluators/__init__.py +++ b/fhirpathpy/engine/evaluators/__init__.py @@ -182,6 +182,7 @@ def func(acc, res): actualTypes = model["choiceTypePaths"][childPath] toAdd = None + toAdd_ = None if isinstance(actualTypes, list): # Use actualTypes to find the field's value @@ -189,11 +190,13 @@ def func(acc, res): field = key + actualType if isinstance(res.data, (dict, list)) and field in res.data: toAdd = res.data[field] + toAdd_ = res.data.get(f"_{field}") childPath = actualType break else: if isinstance(res.data, (dict, list)) and key in res.data: toAdd = res.data[key] + toAdd_ = res.data.get(f"_{key}") if util.is_some(toAdd): if isinstance(toAdd, list): @@ -201,13 +204,19 @@ def func(acc, res): acc = acc + mapped else: acc.append(nodes.ResourceNode.create_node(toAdd, childPath)) - return acc + if util.is_some(toAdd_): + if isinstance(toAdd_, list): + mapped = [nodes.ResourceNode.create_node(x, childPath) for x in toAdd_] + acc = acc + mapped + else: + acc.append(nodes.ResourceNode.create_node(toAdd_, childPath)) return acc return func def member_invocation(ctx, parentData, node): + # print("CTX", [util.get_data(p) for p in parentData]) key = engine.do_eval(ctx, parentData, node["children"][0])[0] model = ctx["model"] @@ -265,9 +274,7 @@ def polarity_expression(ctx, parentData, node): rtn = engine.do_eval(ctx, parentData, node["children"][0]) if len(rtn) != 1: # not yet in spec, but per Bryn Rhodes - raise Exception( - "Unary " + sign + " can only be applied to an individual number." - ) + raise Exception("Unary " + sign + " can only be applied to an individual number.") if not util.is_number(rtn[0]): raise Exception("Unary " + sign + " can only be applied to a number.") @@ -301,9 +308,7 @@ def polarity_expression(ctx, parentData, node): # expressions "PolarityExpression": polarity_expression, "IndexerExpression": indexer_expression, - "MembershipExpression": alias_op_expression( - {"contains": "containsOp", "in": "inOp"} - ), + "MembershipExpression": alias_op_expression({"contains": "containsOp", "in": "inOp"}), "TermExpression": term_expression, "UnionExpression": union_expression, "InvocationExpression": invocation_expression, diff --git a/fhirpathpy/engine/invocations/filtering.py b/fhirpathpy/engine/invocations/filtering.py index 7a8b2b0..2bf33b8 100644 --- a/fhirpathpy/engine/invocations/filtering.py +++ b/fhirpathpy/engine/invocations/filtering.py @@ -116,10 +116,13 @@ def check_fhir_type(ctx, x, tp): def of_type_fn(ctx, coll, tp): return list(filter(lambda x: check_fhir_type(ctx, util.get_data(x), tp), coll)) -def extension(ctx,data,url): - res = [] + +def extension(ctx, data, url): + res = [] for d in data: - exts = [e for e in util.get_data(d)["extension"] if e["url"] == url] - if len(exts) > 0: - res.append(exts[0]) + element = util.get_data(d) + if isinstance(element, dict): + exts = [e for e in element.get("extension", []) if e["url"] == url] + if len(exts) > 0: + res.append(exts[0]) return res From 1cbf0dd09ecf039060a9b32e48ced810288ccb45 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 16:34:15 +0400 Subject: [PATCH 05/12] Enable toDateTime and toTime --- fhirpathpy/engine/invocations/__init__.py | 4 ++-- fhirpathpy/engine/invocations/misc.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fhirpathpy/engine/invocations/__init__.py b/fhirpathpy/engine/invocations/__init__.py index b8f9f9d..6dc247a 100644 --- a/fhirpathpy/engine/invocations/__init__.py +++ b/fhirpathpy/engine/invocations/__init__.py @@ -43,8 +43,8 @@ "toInteger": {"fn": misc.to_integer}, "toDecimal": {"fn": misc.to_decimal}, "toString": {"fn": misc.to_string}, - # toDateTime: {fn: misc.toDateTime}, - # toTime: {fn: misc.toTime}, + "toDateTime": {"fn": misc.to_date_time}, + "toTime": {"fn": misc.to_time}, "indexOf": {"fn": strings.index_of, "arity": {1: ["String"]}, "nullable_input": True}, "substring": { "fn": strings.substring, diff --git a/fhirpathpy/engine/invocations/misc.py b/fhirpathpy/engine/invocations/misc.py index 0431975..9c7b82d 100644 --- a/fhirpathpy/engine/invocations/misc.py +++ b/fhirpathpy/engine/invocations/misc.py @@ -96,7 +96,7 @@ def to_date_time(ctx, coll): dateTimeObject = nodes.FP_DateTime(value) if dateTimeObject: - rtn[0] = dateTimeObject + rtn.append(dateTimeObject) return rtn From 3451a9ab28287533492810e56999b472293b7b7f Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 16:36:31 +0400 Subject: [PATCH 06/12] Fix index error for to_time --- fhirpathpy/engine/invocations/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fhirpathpy/engine/invocations/misc.py b/fhirpathpy/engine/invocations/misc.py index 9c7b82d..bee0a99 100644 --- a/fhirpathpy/engine/invocations/misc.py +++ b/fhirpathpy/engine/invocations/misc.py @@ -113,6 +113,6 @@ def to_time(ctx, coll): timeObject = nodes.FP_Time(value) if timeObject: - rtn[0] = timeObject + rtn.append(timeObject) return rtn From 99c24ad4683942f90374ee2180664346b92fe872 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 18:08:36 +0400 Subject: [PATCH 07/12] Fix date time equality --- fhirpathpy/engine/invocations/equality.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fhirpathpy/engine/invocations/equality.py b/fhirpathpy/engine/invocations/equality.py index 9781a94..5b36f3c 100644 --- a/fhirpathpy/engine/invocations/equality.py +++ b/fhirpathpy/engine/invocations/equality.py @@ -35,9 +35,11 @@ def datetime_equality(ctx, x, y): datetime_x = x[0] datetime_y = y[0] if type(datetime_x) not in DATETIME_NODES_LIST: - datetime_x = nodes.FP_DateTime(datetime_x) or nodes.FP_Time(datetime_x) + v_x = util.get_data(datetime_x) + datetime_x = nodes.FP_DateTime(v_x) or nodes.FP_Time(v_x) if type(datetime_y) not in DATETIME_NODES_LIST: - datetime_y = nodes.FP_DateTime(datetime_y) or nodes.FP_Time(datetime_y) + v_y = util.get_data(datetime_y) + datetime_y = nodes.FP_DateTime(v_y) or nodes.FP_Time(v_y) return datetime_x.equals(datetime_y) From afcc28f625838166709da5122be0b95bcbd6b9dd Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 18:08:55 +0400 Subject: [PATCH 08/12] Override childPath for extensions --- fhirpathpy/engine/evaluators/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fhirpathpy/engine/evaluators/__init__.py b/fhirpathpy/engine/evaluators/__init__.py index 6fe725c..5cfc948 100644 --- a/fhirpathpy/engine/evaluators/__init__.py +++ b/fhirpathpy/engine/evaluators/__init__.py @@ -197,6 +197,8 @@ def func(acc, res): if isinstance(res.data, (dict, list)) and key in res.data: toAdd = res.data[key] toAdd_ = res.data.get(f"_{key}") + if key == 'extension': + childPath = 'Extension' if util.is_some(toAdd): if isinstance(toAdd, list): From 9641ee8ecef46caa9c759ba2ec68d4f877ee3035 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 18:19:02 +0400 Subject: [PATCH 09/12] Add patient-example-2 resource --- tests/resources/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py index 4184ad0..353638c 100644 --- a/tests/resources/__init__.py +++ b/tests/resources/__init__.py @@ -14,6 +14,7 @@ def save_to_resources(resources, resource_filename): save_to_resources(resources, "observation-example.json") save_to_resources(resources, "patient-example.json") +save_to_resources(resources, "patient-example-2.json") save_to_resources(resources, "quantity-example.json") save_to_resources(resources, "questionnaire-example.json") save_to_resources(resources, "valueset-example-expansion.json") From 05852f9f76043bea6ce58d631be7fe0038c9c6a0 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 20:23:52 +0400 Subject: [PATCH 10/12] Return Extension node from extension function --- fhirpathpy/engine/invocations/filtering.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fhirpathpy/engine/invocations/filtering.py b/fhirpathpy/engine/invocations/filtering.py index 2bf33b8..2d516dc 100644 --- a/fhirpathpy/engine/invocations/filtering.py +++ b/fhirpathpy/engine/invocations/filtering.py @@ -1,5 +1,6 @@ import numbers import fhirpathpy.engine.util as util +import fhirpathpy.engine.nodes as nodes # Contains the FHIRPath Filtering and Projection functions. # (Section 5.2 of the FHIRPath 1.0.0 specification). @@ -124,5 +125,5 @@ def extension(ctx, data, url): if isinstance(element, dict): exts = [e for e in element.get("extension", []) if e["url"] == url] if len(exts) > 0: - res.append(exts[0]) + res.append(nodes.ResourceNode.create_node(exts[0], "Extension")) return res From d14eeb6c08dcf2bda1d1af2d0328271f4d6e23a4 Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 20:24:24 +0400 Subject: [PATCH 11/12] Remove debug output --- fhirpathpy/engine/evaluators/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fhirpathpy/engine/evaluators/__init__.py b/fhirpathpy/engine/evaluators/__init__.py index 5cfc948..4ec78cf 100644 --- a/fhirpathpy/engine/evaluators/__init__.py +++ b/fhirpathpy/engine/evaluators/__init__.py @@ -218,7 +218,6 @@ def func(acc, res): def member_invocation(ctx, parentData, node): - # print("CTX", [util.get_data(p) for p in parentData]) key = engine.do_eval(ctx, parentData, node["children"][0])[0] model = ctx["model"] From 528a43e2c453a3e45572a8080ff763b7150e416a Mon Sep 17 00:00:00 2001 From: Ilya Beda Date: Wed, 5 Jul 2023 20:25:09 +0400 Subject: [PATCH 12/12] Fix bug when primitive extension works only when attribute is defined --- fhirpathpy/engine/evaluators/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fhirpathpy/engine/evaluators/__init__.py b/fhirpathpy/engine/evaluators/__init__.py index 4ec78cf..a5e2985 100644 --- a/fhirpathpy/engine/evaluators/__init__.py +++ b/fhirpathpy/engine/evaluators/__init__.py @@ -188,14 +188,15 @@ def func(acc, res): # Use actualTypes to find the field's value for actualType in actualTypes: field = key + actualType - if isinstance(res.data, (dict, list)) and field in res.data: - toAdd = res.data[field] + if isinstance(res.data, (dict, list)): + toAdd = res.data.get(field) toAdd_ = res.data.get(f"_{field}") - childPath = actualType - break + if toAdd is not None or toAdd_ is not None: + childPath = actualType + break else: - if isinstance(res.data, (dict, list)) and key in res.data: - toAdd = res.data[key] + if isinstance(res.data, (dict, list)): + toAdd = res.data.get(key) toAdd_ = res.data.get(f"_{key}") if key == 'extension': childPath = 'Extension'