├── .editorconfig ├── .github └── workflows │ ├── build.yaml │ └── publish.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE.md ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── fhirpathpy ├── __init__.py ├── engine │ ├── __init__.py │ ├── evaluators │ │ └── __init__.py │ ├── invocations │ │ ├── __init__.py │ │ ├── aggregate.py │ │ ├── collections.py │ │ ├── combining.py │ │ ├── constants.py │ │ ├── datetime.py │ │ ├── equality.py │ │ ├── existence.py │ │ ├── filtering.py │ │ ├── logic.py │ │ ├── math.py │ │ ├── misc.py │ │ ├── navigation.py │ │ ├── strings.py │ │ ├── subsetting.py │ │ └── types.py │ ├── nodes.py │ └── util.py ├── models │ ├── __init__.py │ ├── dstu2 │ │ ├── choiceTypePaths.json │ │ ├── path2Type.json │ │ ├── pathsDefinedElsewhere.json │ │ └── type2Parent.json │ ├── r4 │ │ ├── choiceTypePaths.json │ │ ├── path2Type.json │ │ ├── pathsDefinedElsewhere.json │ │ └── type2Parent.json │ ├── r5 │ │ ├── choiceTypePaths.json │ │ ├── path2Type.json │ │ ├── pathsDefinedElsewhere.json │ │ └── type2Parent.json │ └── stu3 │ │ ├── choiceTypePaths.json │ │ ├── path2Type.json │ │ ├── pathsDefinedElsewhere.json │ │ └── type2Parent.json └── parser │ ├── ASTPathListener.py │ ├── FHIRPath.g4 │ ├── README.md │ ├── __init__.py │ └── generated │ ├── FHIRPath.interp │ ├── FHIRPath.tokens │ ├── FHIRPathLexer.interp │ ├── FHIRPathLexer.py │ ├── FHIRPathLexer.tokens │ ├── FHIRPathListener.py │ ├── FHIRPathParser.py │ └── __init__.py ├── pyproject.toml └── tests ├── __init__.py ├── cases ├── 3.2_paths.yaml ├── 4.1_literals.yaml ├── 5.1_existence.yaml ├── 5.2.3_repeat.yaml ├── 5.2_filtering_and_projection.yaml ├── 5.3_subsetting.yaml ├── 5.4_combining.yaml ├── 5.5_conversion.yaml ├── 5.6_string_manipulation.yaml ├── 5.7_math.yaml ├── 5.8_tree_navigation.yaml ├── 5.9_utility_functions.yaml ├── 6.1_equality.yaml ├── 6.2_comparision.yaml ├── 6.3_types.yaml ├── 6.4_collection.yaml ├── 6.4_collections.yaml ├── 6.5_boolean_logic.yaml ├── 6.6_math.yaml ├── 7_aggregate.yaml ├── 8_variables.yaml ├── extensions.yaml ├── fhir-quantity.yaml ├── fhir-r4.yaml └── simple.yaml ├── conftest.py ├── fixtures └── ast │ ├── %v+2.json │ ├── Observation.value.json │ ├── Patient.name.given.json │ └── a.b+2.json ├── resources ├── Minimum-Data-Set---version-3.0.R4.json ├── __init__.py ├── medicationrequest-example.json ├── observation-example.json ├── patient-example-2.json ├── patient-example.json ├── quantity-example.json ├── questionnaire-example.json ├── questionnaire-part-example.json └── valueset-example-expansion.json ├── test_additional.py ├── test_equivalence.py ├── test_evaluators.py ├── test_parser.py └── test_real.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | # Docstrings and comments use max_line_length = 79 14 | [*.py] 15 | max_line_length = 119 16 | 17 | # Use 2 spaces for the HTML files 18 | [*.html] 19 | indent_size = 2 20 | 21 | # The JSON files contain newlines inconsistently 22 | [*.json] 23 | indent_size = 2 24 | insert_final_newline = ignore 25 | 26 | [**/admin/js/vendor/**] 27 | indent_style = ignore 28 | indent_size = ignore 29 | 30 | # Minified JavaScript files shouldn't be changed 31 | [**.min.js] 32 | indent_style = ignore 33 | insert_final_newline = ignore 34 | 35 | # Makefiles always use tabs for indentation 36 | [Makefile] 37 | indent_style = tab 38 | 39 | # Batch files use tabs for indentation 40 | [*.bat] 41 | indent_style = tab 42 | 43 | [docs/**.txt] 44 | max_line_length = 79 45 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: fhirpath py 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | include: 13 | - python-version: 3.9 14 | - python-version: "3.10" 15 | - python-version: "3.11" 16 | - python-version: "3.12" 17 | env: 18 | PYTHON: ${{ matrix.python-version }} 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Upgrade pip and pipenv 26 | run: python -m pip install --upgrade pip pipenv 27 | - name: Install dependencies 28 | run: pipenv install --dev 29 | - name: Run tests 30 | run: pipenv run pytest 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v3 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | env_vars: PYTHON 36 | fail_ci_if_error: true 37 | files: ./coverage.xml 38 | flags: unittests 39 | name: codecov-umbrella 40 | verbose: true 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to https://pypi.org/ 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | packages: write 12 | contents: read 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 3.11 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.11" 19 | - name: Install wheel and build 20 | run: pip install wheel build 21 | - name: Build a binary wheel and a source tarball 22 | run: python -m build 23 | - name: Publish package 24 | uses: pypa/gh-action-pypi-publish@release/v1 25 | with: 26 | user: __token__ 27 | password: ${{ secrets.PYPI_API_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Other 132 | .DS_Store 133 | .idea/ 134 | 135 | .history/ 136 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.12.0 4 | hooks: 5 | - id: black-jupyter 6 | language_version: python3.11 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.3.0 9 | hooks: 10 | - id: check-yaml 11 | - id: check-toml 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.2 2 | 3 | - Increase performance of parsing #54 @ruscoder 4 | 5 | ## 2.0.1 6 | 7 | - Fix of the bug with multiple user-defined functions @kpcurai @ruscoder 8 | 9 | ## 2.0.0 10 | 11 | - Raise an error in case of accessing undefined env variable #50 @ruscoder 12 | - Add support for user defined functions #49 @ruscoder 13 | 14 | ## 1.2.1 15 | 16 | - Upgrade paython-dateutil #46 @kpcurai 17 | 18 | ## 1.2.0 19 | 20 | - Support collection.abc.Mapping as resource instead of only dict #44 @axelv 21 | 22 | ## 1.0.0 23 | 24 | - Implement all fhirpath specification, pass all tests from fhirpath-js @atuonufure 25 | 26 | ## 0.2.2 27 | 28 | - Fix bug with $this calculation @ir4y 29 | 30 | ## 0.2.1 31 | 32 | - Issue 21 by @ir4y in #22 Add extensions support 33 | 34 | ## 0.1.2 35 | 36 | - Setup automatice releases with github actions 37 | 38 | ## 0.1.1 39 | 40 | - Fix datetime functions #19 41 | 42 | ## 0.1.0 43 | 44 | - Initial release 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 beda.software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.md 3 | recursive-include fhirpathpy * 4 | recursive-exclude * __pycache__ 5 | recursive-exclude * *.py[co] 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | antlr4-python3-runtime = "~=4.10" 8 | python-dateutil = "~=2.8" 9 | 10 | [dev-packages] 11 | pytest = "==7.1.1" 12 | pytest-cov = "==3.0.0" 13 | pyyaml = "==6.0.1" 14 | pre-commit = "~=2.21.0" 15 | ipython = "*" 16 | black = "*" 17 | types-python-dateutil = "*" 18 | ruff = "==0.6.3" 19 | autohooks = "*" 20 | autohooks-plugin-black = "*" 21 | autohooks-plugin-ruff = "*" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fhirpath.py 2 | =========== 3 | 4 | [![Build Status](https://github.com/beda-software/fhirpath-py/actions/workflows/build.yaml/badge.svg)](https://github.com/beda-software/fhirpath-py/actions) 5 | [![codecov](https://codecov.io/gh/beda-software/fhirpath-py/branch/master/graph/badge.svg)](https://codecov.io/gh/beda-software/fhirpath-py) 6 | [![pypi](https://img.shields.io/pypi/v/fhirpathpy.svg)](https://pypi.org/project/fhirpathpy/) 7 | [![Supported Python version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/release/python-380/) 8 | 9 | [FHIRPath](https://www.hl7.org/fhir/fhirpath.html) implementation in Python 10 | 11 | Parser was generated with [antlr4](https://github.com/antlr/antlr4) 12 | 13 | # Getting started 14 | ## Install 15 | `pip install fhirpathpy` 16 | 17 | ## Usage 18 | ```Python 19 | from fhirpathpy import evaluate 20 | 21 | patient = { 22 | "resourceType": "Patient", 23 | "id": "example", 24 | "name": [ 25 | { 26 | "use": "official", 27 | "given": [ 28 | "Peter", 29 | "James" 30 | ], 31 | "family": "Chalmers" 32 | }, 33 | { 34 | "use": "usual", 35 | "given": [ 36 | "Jim" 37 | ] 38 | }, 39 | { 40 | "use": "maiden", 41 | "given": [ 42 | "Peter", 43 | "James" 44 | ], 45 | "family": "Windsor", 46 | "period": { 47 | "end": "2002" 48 | } 49 | } 50 | ] 51 | } 52 | 53 | # Evaluating FHIRPath 54 | result = evaluate(patient, "Patient.name.where(use='usual').given.first()", {}) 55 | # result: `['Jim']` 56 | ``` 57 | 58 | ## evaluate 59 | Evaluates the "path" FHIRPath expression on the given resource, using data from "context" for variables mentioned in the "path" expression. 60 | 61 | **Parameters** 62 | 63 | resource (dict|list): FHIR resource, bundle as js object or array of resources This resource will be modified by this function to add type information. 64 | 65 | path (string): fhirpath expression, sample 'Patient.name.given' 66 | 67 | context (dict): a hash of variable name/value pairs. 68 | 69 | model (dict): The "model" data object specific to a domain, e.g. R4. See Using data models documentation below. 70 | 71 | options (dict) - Custom options. 72 | 73 | options.userInvocationTable - a user invocation table used to replace any existing functions or define new ones. See User-defined functions documentation below. 74 | 75 | ## compile 76 | Returns a function that takes a resource and an optional context hash (see "evaluate"), and returns the result of evaluating the given FHIRPath expression on that resource. The advantage of this function over "evaluate" is that if you have multiple resources, the given FHIRPath expression will only be parsed once 77 | 78 | **Parameters** 79 | 80 | path (string) - the FHIRPath expression to be parsed. 81 | 82 | model (dict) - The "model" data object specific to a domain, e.g. R4. See Using data models documentation below. 83 | 84 | options (dict) - Custom options 85 | 86 | options.userInvocationTable - a user invocation table used to replace any existing functions or define new ones. See User-defined functions documentation below. 87 | 88 | ## Using data models 89 | 90 | The fhirpathpy library comes with pre-defined data models for FHIR versions DSTU2, STU3, R4, and R5. These models can be used within your project. 91 | 92 | Example: 93 | ```python 94 | from fhirpathpy.models import models 95 | 96 | 97 | r4_model = models["r4"] 98 | 99 | patient = { 100 | "resourceType": "Patient", 101 | "deceasedBoolean": false, 102 | } 103 | 104 | result = evaluate(patient, "Patient.deceased", {}, r4_model) 105 | 106 | # result: `[False]` 107 | ``` 108 | 109 | ## User-defined functions 110 | 111 | The FHIRPath specification includes a set of built-in functions. However, if you need to extend the functionality by adding custom logic, you can define your own functions by providing a table of user-defined functions. 112 | 113 | Example: 114 | ```python 115 | user_invocation_table = { 116 | "pow": { 117 | "fn": lambda inputs, exp=2: [i**exp for i in inputs], 118 | "arity": {0: [], 1: ["Integer"]}, 119 | } 120 | } 121 | 122 | result = evaluate( 123 | {"a": [5, 6, 7]}, 124 | "a.pow()", 125 | options={"userInvocationTable": user_invocation_table}, 126 | ) 127 | 128 | # result: `[25, 36, 49]` 129 | ``` 130 | 131 | It works similarly to [fhirpath.js](https://github.com/HL7/fhirpath.js/tree/master?tab=readme-ov-file#user-defined-functions) 132 | 133 | 134 | ## Development 135 | 136 | To activate git pre-commit hook: `autohooks activate` 137 | 138 | To run tests: `pytest` 139 | -------------------------------------------------------------------------------- /fhirpathpy/__init__.py: -------------------------------------------------------------------------------- 1 | from fhirpathpy.engine.invocations.constants import constants 2 | from fhirpathpy.parser import parse 3 | from fhirpathpy.engine import do_eval 4 | from fhirpathpy.engine.util import arraify, get_data, set_paths, process_user_invocation_table 5 | from fhirpathpy.engine.nodes import FP_Type, ResourceNode 6 | 7 | __title__ = "fhirpathpy" 8 | __version__ = "2.0.2" 9 | __author__ = "beda.software" 10 | __license__ = "MIT" 11 | __copyright__ = "Copyright 2025 beda.software" 12 | 13 | # Version synonym 14 | VERSION = __version__ 15 | 16 | 17 | def apply_parsed_path(resource, parsedPath, context=None, model=None, options=None): 18 | constants.reset() 19 | dataRoot = arraify(resource) 20 | 21 | """ 22 | do_eval takes a "ctx" object, and we store things in that as we parse, so we 23 | need to put user-provided variable data in a sub-object, ctx['vars']. 24 | Set up default standard variables, and allow override from the variables. 25 | However, we'll keep our own copy of dataRoot for internal processing. 26 | """ 27 | vars = {"context": resource, "ucum": "http://unitsofmeasure.org"} 28 | vars.update(context or {}) 29 | 30 | ctx = { 31 | "dataRoot": dataRoot, 32 | "vars": vars, 33 | "model": model, 34 | "userInvocationTable": process_user_invocation_table( 35 | (options or {}).get("userInvocationTable", {}) 36 | ), 37 | } 38 | node = do_eval(ctx, dataRoot, parsedPath["children"][0]) 39 | 40 | # Resolve any internal "ResourceNode" instances. Continue to let FP_Type 41 | # subclasses through. 42 | 43 | def visit(node): 44 | data = get_data(node) 45 | 46 | if isinstance(node, list): 47 | res = [] 48 | for item in data: 49 | # Filter out intenal representation of primitive extensions 50 | i = visit(item) 51 | if isinstance(i, dict): 52 | keys = list(i.keys()) 53 | if keys == ["extension"]: 54 | continue 55 | res.append(i) 56 | return res 57 | 58 | if isinstance(data, dict) and not isinstance(data, FP_Type): 59 | for key, value in data.items(): 60 | data[key] = visit(value) 61 | 62 | return data 63 | 64 | return visit(node) 65 | 66 | 67 | def evaluate(resource, path, context=None, model=None, options=None): 68 | """ 69 | Evaluates the "path" FHIRPath expression on the given resource, using data 70 | from "context" for variables mentioned in the "path" expression. 71 | 72 | Parameters: 73 | resource (dict|list): FHIR resource, bundle as js object or array of resources This resource will be modified by this function to add type information. 74 | path (string): fhirpath expression, sample 'Patient.name.given' 75 | context (dict): a hash of variable name/value pairs. 76 | model (dict): The "model" data object specific to a domain, e.g. R4. 77 | 78 | Returns: 79 | int: Description of return value 80 | 81 | """ 82 | if isinstance(path, dict): 83 | node = parse(path["expression"]) 84 | if "base" in path: 85 | resource = ResourceNode.create_node(resource, path["base"]) 86 | else: 87 | node = parse(path) 88 | 89 | return apply_parsed_path(resource, node, context or {}, model, options) 90 | 91 | 92 | def compile(path, model=None, options=None): 93 | """ 94 | Returns a function that takes a resource and an optional context hash (see 95 | "evaluate"), and returns the result of evaluating the given FHIRPath 96 | expression on that resource. The advantage of this function over "evaluate" 97 | is that if you have multiple resources, the given FHIRPath expression will 98 | only be parsed once. 99 | 100 | Parameters: 101 | path (string) - the FHIRPath expression to be parsed. 102 | model (dict) - The "model" data object specific to a domain, e.g. R4. 103 | 104 | For example, you could pass in the result of require("fhirpath/fhir-context/r4") 105 | """ 106 | return set_paths(apply_parsed_path, parsedPath=parse(path), model=model, options=options) 107 | -------------------------------------------------------------------------------- /fhirpathpy/engine/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numbers 3 | import fhirpathpy.engine.util as util 4 | from fhirpathpy.engine.nodes import TypeInfo 5 | from fhirpathpy.engine.evaluators import evaluators 6 | from fhirpathpy.engine.invocations import ( 7 | invocation_registry as base_invocation_registry, 8 | ) 9 | 10 | 11 | def check_integer_param(val): 12 | data = util.get_data(val) 13 | if int(data) != data: 14 | raise Exception("Expected integer, got: " + json.dumps(data)) 15 | return data 16 | 17 | 18 | def check_number_param(val): 19 | data = util.get_data(val) 20 | if not isinstance(data, numbers.Number): 21 | raise Exception("Expected number, got: " + json.dumps(data)) 22 | return data 23 | 24 | 25 | def check_boolean_param(val): 26 | data = util.get_data(val) 27 | if data is True or data is False: 28 | return data 29 | raise Exception("Expected boolean, got: " + json.dumps(data)) 30 | 31 | 32 | def check_string_param(val): 33 | data = util.get_data(val) 34 | if not isinstance(data, str): 35 | raise Exception("Expected string, got: " + json.dumps(data)) 36 | return data 37 | 38 | 39 | def do_eval(ctx, parentData, node): 40 | node_type = node["type"] 41 | 42 | if node_type in evaluators: 43 | evaluator = evaluators.get(node_type) 44 | return evaluator(ctx, parentData, node) 45 | 46 | raise Exception("No " + node_type + " evaluator ") 47 | 48 | 49 | def doInvoke(ctx, fn_name, data, raw_params): 50 | invocation_registry = { 51 | **base_invocation_registry, 52 | **(ctx["userInvocationTable"] or {}), 53 | } 54 | 55 | if isinstance(fn_name, list) and len(fn_name) == 1: 56 | fn_name = fn_name[0] 57 | 58 | if not isinstance(fn_name, str) or fn_name not in invocation_registry: 59 | raise Exception("Not implemented: " + str(fn_name)) 60 | 61 | invocation = invocation_registry[fn_name] 62 | 63 | if "nullable_input" in invocation and util.is_nullable(data): 64 | return [] 65 | 66 | if "arity" not in invocation: 67 | if raw_params is None or util.is_empty(raw_params): 68 | res = invocation["fn"](ctx, util.arraify(data)) 69 | return util.arraify(res) 70 | 71 | raise Exception(fn_name + " expects no params") 72 | 73 | if invocation["fn"].__name__ == "trace_fn" and raw_params is not None: 74 | raw_params = raw_params[:1] 75 | 76 | paramsNumber = 0 77 | if isinstance(raw_params, list): 78 | paramsNumber = len(raw_params) 79 | 80 | if paramsNumber not in invocation["arity"]: 81 | raise Exception(fn_name + " wrong arity: got " + str(paramsNumber)) 82 | 83 | params = [] 84 | argTypes = invocation["arity"][paramsNumber] 85 | 86 | for i in range(0, paramsNumber): 87 | tp = argTypes[i] 88 | pr = raw_params[i] 89 | params.append(make_param(ctx, data, tp, pr)) 90 | 91 | params.insert(0, data) 92 | params.insert(0, ctx) 93 | 94 | if "nullable" in invocation: 95 | if any(util.is_nullable(x) for x in params): 96 | return [] 97 | 98 | res = invocation["fn"](*params) 99 | 100 | return util.arraify(res) 101 | 102 | 103 | def type_specifier(ctx, parent_data, node): 104 | identifiers = node["text"].replace("`", "").split(".") 105 | namespace = None 106 | name = None 107 | 108 | if len(identifiers) == 2: 109 | namespace, name = identifiers 110 | elif len(identifiers) == 1: 111 | (name,) = identifiers 112 | else: 113 | raise Exception(f"Expected TypeSpecifier node, got {node}") 114 | 115 | return TypeInfo(name=name, namespace=namespace) 116 | 117 | 118 | param_check_table = { 119 | "Integer": check_integer_param, 120 | "Number": check_number_param, 121 | "Boolean": check_boolean_param, 122 | "String": check_string_param, 123 | } 124 | 125 | 126 | def make_param(ctx, parentData, node_type, param): 127 | if node_type == "Expr": 128 | 129 | def func(data): 130 | ctx["$this"] = util.arraify(data) 131 | return do_eval(ctx, ctx["$this"], param) 132 | 133 | return func 134 | 135 | if node_type == "AnyAtRoot": 136 | ctx["$this"] = ctx["$this"] if "$this" in ctx else ctx["dataRoot"] 137 | return do_eval(ctx, ctx["$this"], param) 138 | 139 | if node_type == "Identifier": 140 | if param["type"] == "TermExpression": 141 | return param["text"] 142 | 143 | raise Exception("Expected identifier node, got " + json.dumps(param)) 144 | 145 | if node_type == "TypeSpecifier": 146 | return type_specifier(ctx, parentData, param) 147 | 148 | ctx["$this"] = parentData 149 | res = do_eval(ctx, parentData, param) 150 | 151 | if node_type == "Any": 152 | return res 153 | 154 | if isinstance(node_type, list): 155 | if len(res) == 0: 156 | return [] 157 | else: 158 | node_type = node_type[0] 159 | 160 | if len(res) > 1: 161 | raise Exception( 162 | "Unexpected collection" 163 | + json.dumps(res) 164 | + "; expected singleton of type " 165 | + node_type 166 | ) 167 | 168 | if len(res) == 0: 169 | return [] 170 | 171 | if node_type not in param_check_table: 172 | raise Exception("Implement me for " + node_type) 173 | 174 | check = param_check_table[node_type] 175 | 176 | return check(res[0]) 177 | 178 | 179 | def infix_invoke(ctx, fn_name, data, raw_params): 180 | invocation_registry = { 181 | **base_invocation_registry, 182 | **(ctx["userInvocationTable"] or {}), 183 | } 184 | if fn_name not in invocation_registry or "fn" not in invocation_registry[fn_name]: 185 | raise Exception("Not implemented " + fn_name) 186 | 187 | invocation = invocation_registry[fn_name] 188 | paramsNumber = len(raw_params) 189 | 190 | if paramsNumber != 2: 191 | raise Exception("Infix invoke should have arity 2") 192 | 193 | argTypes = invocation["arity"][paramsNumber] 194 | 195 | if argTypes is not None: 196 | params = [ctx] 197 | 198 | for i in range(0, paramsNumber): 199 | argType = argTypes[i] 200 | rawParam = raw_params[i] 201 | params.append(make_param(ctx, data, argType, rawParam)) 202 | 203 | if "nullable" in invocation: 204 | if any(util.is_nullable(x) for x in params): 205 | return [] 206 | 207 | res = invocation["fn"](*params) 208 | return util.arraify(res) 209 | 210 | print(fn_name + " wrong arity: got " + paramsNumber) 211 | return [] 212 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/aggregate.py: -------------------------------------------------------------------------------- 1 | from fhirpathpy.engine.invocations.existence import count_fn 2 | from fhirpathpy.engine.invocations.math import div 3 | 4 | 5 | def avg_fn(ctx, x): 6 | if count_fn(ctx, x) == 0: 7 | return [] 8 | 9 | return div(ctx, sum_fn(ctx, x), count_fn(ctx, x)) 10 | 11 | 12 | def sum_fn(ctx, x): 13 | return sum(x) 14 | 15 | 16 | def min_fn(ctx, x): 17 | if count_fn(ctx, x) == 0: 18 | return [] 19 | 20 | return min(x) 21 | 22 | 23 | def max_fn(ctx, x): 24 | if count_fn(ctx, x) == 0: 25 | return [] 26 | 27 | return max(x) 28 | 29 | 30 | def aggregate_macro(ctx, data, expr, initial_value=None): 31 | ctx["$total"] = initial_value 32 | for i, x in enumerate(data): 33 | ctx["$index"] = i 34 | ctx["$total"] = expr(x) 35 | return ctx["$total"] 36 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/collections.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file holds code to hande the FHIRPath Math functions. 3 | """ 4 | 5 | 6 | def contains_impl(ctx, a, b): 7 | # b is assumed to have one element and it tests whether b[0] is in a 8 | if len(b) == 0: 9 | return True 10 | 11 | for i in range(0, len(a)): 12 | if a[i] == b[0]: 13 | return True 14 | 15 | return False 16 | 17 | 18 | def contains(ctx, a, b): 19 | if len(b) == 0: 20 | return [] 21 | if len(a) == 0: 22 | return False 23 | if len(b) > 1: 24 | raise Exception("Expected singleton on right side of contains, got " + str(b)) 25 | 26 | return contains_impl(ctx, a, b) 27 | 28 | 29 | def inn(ctx, a, b): 30 | if len(a) == 0: 31 | return [] 32 | if len(b) == 0: 33 | return False 34 | if len(a) > 1: 35 | raise Exception("Expected singleton on right side of in, got " + str(b)) 36 | 37 | return contains_impl(ctx, b, a) 38 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/combining.py: -------------------------------------------------------------------------------- 1 | import fhirpathpy.engine.invocations.existence as existence 2 | 3 | """ 4 | This file holds code to hande the FHIRPath Combining functions 5 | """ 6 | 7 | 8 | def union_op(ctx, coll1, coll2): 9 | return existence.distinct_fn(ctx, coll1 + coll2) 10 | 11 | 12 | def combine_fn(ctx, coll1, coll2): 13 | return coll1 + coll2 14 | 15 | 16 | def exclude_fn(ctx, coll1, coll2): 17 | return [element for element in coll1 if element not in coll2] 18 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/constants.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class SystemTime: 5 | """ 6 | System date/time should not change during an evaluation of a FHIRPath 7 | expression. It remains the same for the entire expression evaluation. 8 | """ 9 | 10 | def __init__(self) -> None: 11 | self.expressionExecutionDateTime = datetime.now() 12 | 13 | def now(self): 14 | return self.expressionExecutionDateTime 15 | 16 | def reset(self): 17 | self.expressionExecutionDateTime = datetime.now() 18 | 19 | 20 | class Constants: 21 | """ 22 | These are values that should not change during an evaluation of a FHIRPath 23 | expression (e.g. the return value of today(), per the spec.) They are 24 | constant during at least one evaluation. 25 | """ 26 | 27 | today = None 28 | now = None 29 | timeOfDay = None 30 | localTimezoneOffset = None 31 | 32 | def reset(self): 33 | self.today = None 34 | self.now = None 35 | self.timeOfDay = None 36 | self.localTimezoneOffset = None 37 | systemtime.reset() 38 | 39 | 40 | constants = Constants() 41 | systemtime = SystemTime() 42 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/datetime.py: -------------------------------------------------------------------------------- 1 | from fhirpathpy.engine.invocations.constants import constants, systemtime 2 | from fhirpathpy.engine.nodes import FP_DateTime, FP_Time 3 | 4 | 5 | def now(ctx, data): 6 | if not constants.now: 7 | now = systemtime.now() 8 | if not now.tzinfo: 9 | now = now.astimezone() 10 | isoStr = now.isoformat() # YYYY-MM-DDThh:mm:ss.ffffff+zz:zz 11 | constants.now = str(FP_DateTime(isoStr)) 12 | return constants.now 13 | 14 | 15 | def today(ctx, data): 16 | if not constants.today: 17 | now = systemtime.now() 18 | isoStr = now.date().isoformat() # YYYY-MM-DD 19 | constants.today = str(FP_DateTime(isoStr)) 20 | return constants.today 21 | 22 | 23 | def timeOfDay(ctx, data): 24 | if not constants.timeOfDay: 25 | now = systemtime.now() 26 | isoStr = now.time().isoformat() # hh:mm:ss.ffffff 27 | constants.timeOfDay = str(FP_Time(isoStr)) 28 | return constants.timeOfDay 29 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/existence.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from fhirpathpy.engine.invocations import misc 3 | from fhirpathpy.engine.invocations.misc import to_boolean 4 | import fhirpathpy.engine.util as util 5 | import fhirpathpy.engine.nodes as nodes 6 | import fhirpathpy.engine.invocations.filtering as filtering 7 | 8 | 9 | """ 10 | This file holds code to hande the FHIRPath Existence functions 11 | (5.1 in the specification). 12 | """ 13 | 14 | 15 | def empty_fn(ctx, value): 16 | return util.is_empty(value) 17 | 18 | 19 | def count_fn(ctx, value): 20 | if isinstance(value, list): 21 | return len(value) 22 | return 0 23 | 24 | 25 | def not_fn(ctx, x): 26 | if len(x) != 1: 27 | return [] 28 | 29 | data = util.get_data(x[0]) 30 | data = misc.singleton(x, "Boolean") 31 | 32 | if isinstance(data, bool): 33 | return not data 34 | 35 | return [] 36 | 37 | 38 | def exists_macro(ctx, coll, expr=None): 39 | vec = coll 40 | if expr is not None: 41 | return exists_macro(ctx, filtering.where_macro(ctx, coll, expr)) 42 | 43 | return not util.is_empty(vec) 44 | 45 | 46 | def all_macro(ctx, colls, expr): 47 | for i, coll in enumerate(colls): 48 | ctx["$index"] = i 49 | if not util.is_true(expr(coll)): 50 | return [False] 51 | 52 | return [True] 53 | 54 | 55 | def extract_boolean_value(data): 56 | value = util.get_data(data) 57 | if type(value) != bool: 58 | raise Exception("Found type '" + type(data) + "' but was expecting bool") 59 | return value 60 | 61 | 62 | def all_true_fn(ctx, items): 63 | return [all(extract_boolean_value(item) for item in items)] 64 | 65 | 66 | def any_true_fn(ctx, items): 67 | return [any(extract_boolean_value(item) for item in items)] 68 | 69 | 70 | def all_false_fn(ctx, items): 71 | return [all(not extract_boolean_value(item) for item in items)] 72 | 73 | 74 | def any_false_fn(ctx, items): 75 | return [any(not extract_boolean_value(item) for item in items)] 76 | 77 | 78 | def subset_of(ctx, coll1, coll2): 79 | return all(item in coll2 for item in coll1) 80 | 81 | 82 | def subset_of_fn(ctx, coll1, coll2): 83 | return [subset_of(ctx, coll1, coll2)] 84 | 85 | 86 | def superset_of_fn(ctx, coll1, coll2): 87 | return [subset_of(ctx, coll2, coll1)] 88 | 89 | 90 | def distinct_fn(ctx, x): 91 | conversion_factors = { 92 | "weeks": Decimal("604800000"), 93 | "'wk'": Decimal("604800000"), 94 | "week": Decimal("604800000"), 95 | "days": Decimal("86400000"), 96 | "'d'": Decimal("86400000"), 97 | "day": Decimal("86400000"), 98 | "hours": Decimal("3600000"), 99 | "'h'": Decimal("3600000"), 100 | "hour": Decimal("3600000"), 101 | "minutes": Decimal("60000"), 102 | "'min'": Decimal("60000"), 103 | "minute": Decimal("60000"), 104 | "seconds": Decimal("1000"), 105 | "'s'": Decimal("1000"), 106 | "second": Decimal("1000"), 107 | "milliseconds": Decimal("1"), 108 | "'ms'": Decimal("1"), 109 | "millisecond": Decimal("1"), 110 | "years": Decimal("12"), 111 | "'a'": Decimal("12"), 112 | "year": Decimal("12"), 113 | "months": Decimal("1"), 114 | "'mo'": Decimal("1"), 115 | "month": Decimal("1"), 116 | } 117 | 118 | if all(isinstance(v, nodes.ResourceNode) for v in x): 119 | data = [v.data for v in x] 120 | unique = util.uniq(data) 121 | return [nodes.ResourceNode.create_node(item) for item in unique] 122 | 123 | if all(isinstance(v, nodes.FP_Quantity) for v in x): 124 | converted_values = {} 125 | original_values = {} 126 | 127 | for interval in x: 128 | unit = interval.unit 129 | if unit in conversion_factors: 130 | converted_value = interval.value * conversion_factors[unit] 131 | if converted_value not in converted_values: 132 | converted_values[converted_value] = interval.value 133 | original_values[converted_value] = interval 134 | 135 | if len(converted_values) == 1: 136 | return [list(original_values.values())[0]] 137 | 138 | return [original_values[val] for val in util.uniq(converted_values.values())] 139 | 140 | return util.uniq(x) 141 | 142 | 143 | def isdistinct_fn(ctx, x): 144 | return [len(x) == len(distinct_fn(ctx, x))] 145 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/filtering.py: -------------------------------------------------------------------------------- 1 | from collections import abc 2 | from decimal import Decimal 3 | import numbers 4 | import fhirpathpy.engine.util as util 5 | import fhirpathpy.engine.nodes as nodes 6 | 7 | # Contains the FHIRPath Filtering and Projection functions. 8 | # (Section 5.2 of the FHIRPath 1.0.0 specification). 9 | 10 | """ 11 | Adds the filtering and projection functions to the given FHIRPath engine. 12 | """ 13 | 14 | 15 | def check_macro_expr(expr, x): 16 | result = expr(x) 17 | if len(result) > 0: 18 | return expr(x)[0] 19 | 20 | return False 21 | 22 | 23 | def where_macro(ctx, data, expr): 24 | if not isinstance(data, list): 25 | return [] 26 | 27 | result = [] 28 | 29 | for i, x in enumerate(data): 30 | ctx["$index"] = i 31 | if check_macro_expr(expr, x): 32 | result.append(x) 33 | 34 | return util.flatten(result) 35 | 36 | 37 | def select_macro(ctx, data, expr): 38 | if not isinstance(data, list): 39 | return [] 40 | 41 | result = [] 42 | 43 | for i, x in enumerate(data): 44 | ctx["$index"] = i 45 | result.append(expr(x)) 46 | 47 | return util.flatten(result) 48 | 49 | 50 | def repeat_macro(ctx, data, expr): 51 | if not isinstance(data, list): 52 | return [] 53 | 54 | res = [] 55 | items = data 56 | 57 | next = None 58 | lres = None 59 | 60 | uniq = set() 61 | 62 | while len(items) != 0: 63 | next = items[0] 64 | items = items[1:] 65 | lres = [l for l in expr(next) if l not in uniq] 66 | if len(lres) > 0: 67 | for l in lres: 68 | uniq.add(l) 69 | res = res + lres 70 | items = items + lres 71 | 72 | return res 73 | 74 | 75 | # TODO: behavior on object? 76 | def single_fn(ctx, x): 77 | if len(x) == 1: 78 | return x 79 | 80 | if len(x) == 0: 81 | return [] 82 | 83 | # TODO: should throw error? 84 | return {"$status": "error", "$error": "Expected single"} 85 | 86 | 87 | def first_fn(ctx, x): 88 | if len(x) == 0: 89 | return [] 90 | return x[0] 91 | 92 | 93 | def last_fn(ctx, x): 94 | if len(x) == 0: 95 | return [] 96 | return x[-1] 97 | 98 | 99 | def tail_fn(ctx, x): 100 | if len(x) == 0: 101 | return [] 102 | return x[1:] 103 | 104 | 105 | def take_fn(ctx, x, n): 106 | if len(x) == 0: 107 | return [] 108 | return x[: int(n)] 109 | 110 | 111 | def skip_fn(ctx, x, n): 112 | if len(x) == 0: 113 | return [] 114 | return x[int(n) :] 115 | 116 | 117 | def of_type_fn(ctx, coll, tp): 118 | return [value for value in coll if nodes.TypeInfo.from_value(value).is_(tp)] 119 | 120 | 121 | def extension(ctx, data, url): 122 | res = [] 123 | for d in data: 124 | element = util.get_data(d) 125 | if isinstance(element, abc.Mapping): 126 | exts = [e for e in element.get("extension", []) if e["url"] == url] 127 | if len(exts) > 0: 128 | res.append(nodes.ResourceNode.create_node(exts[0], "Extension")) 129 | return res 130 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/logic.py: -------------------------------------------------------------------------------- 1 | def or_op(ctx, a, b): 2 | if isinstance(b, list): 3 | if a == True: 4 | return True 5 | if a == False: 6 | return [] 7 | if isinstance(a, list): 8 | return [] 9 | if isinstance(a, list): 10 | if b == True: 11 | return True 12 | return [] 13 | 14 | return a or b 15 | 16 | 17 | def and_op(ctx, a, b): 18 | if isinstance(b, list): 19 | if a == True: 20 | return [] 21 | if a == False: 22 | return False 23 | if isinstance(a, list): 24 | return [] 25 | 26 | if isinstance(a, list): 27 | if b == True: 28 | return [] 29 | return False 30 | 31 | return a and b 32 | 33 | 34 | def xor_op(ctx, a, b): 35 | # If a or b are arrays, they must be the empty set. 36 | # In that case, the result is always the empty set. 37 | if isinstance(a, list) or isinstance(b, list): 38 | return [] 39 | 40 | return (a and not b) or (not a and b) 41 | 42 | 43 | def implies_op(ctx, a, b): 44 | if isinstance(b, list): 45 | if a == True: 46 | return [] 47 | if a == False: 48 | return True 49 | if isinstance(a, list): 50 | return [] 51 | 52 | if isinstance(a, list): 53 | if b == True: 54 | return True 55 | return [] 56 | 57 | if a == False: 58 | return True 59 | 60 | return a and b 61 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/math.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from fhirpathpy.engine.invocations.equality import remove_duplicate_extension 3 | import fhirpathpy.engine.util as util 4 | import fhirpathpy.engine.nodes as nodes 5 | 6 | """ 7 | Adds the math functions to the given FHIRPath engine. 8 | """ 9 | 10 | 11 | def is_empty(x): 12 | if util.is_number(x): 13 | return False 14 | return util.is_empty(x) 15 | 16 | 17 | def ensure_number_singleton(x): 18 | data = util.get_data(x) 19 | if isinstance(data, float): 20 | data = Decimal(data) 21 | 22 | if not util.is_number(data): 23 | if not isinstance(data, list) or len(data) != 1: 24 | raise Exception("Expected list with number, but got " + str(data)) 25 | 26 | value = util.get_data(data[0]) 27 | 28 | if isinstance(value, float): 29 | value = Decimal(value) 30 | 31 | if not util.is_number(value): 32 | raise Exception("Expected number, but got " + str(x)) 33 | 34 | return value 35 | return data 36 | 37 | 38 | def amp(ctx, x="", y=""): 39 | if isinstance(x, list) and not x: 40 | x = "" 41 | if isinstance(y, list) and not y: 42 | y = "" 43 | return x + y 44 | 45 | 46 | def minus(ctx, xs_, ys_): 47 | xs = remove_duplicate_extension(xs_) 48 | ys = remove_duplicate_extension(ys_) 49 | 50 | if len(xs) != 1 or len(ys) != 1: 51 | raise Exception("Cannot " + str(xs) + " - " + str(ys)) 52 | 53 | x = util.get_data(util.val_data_converted(xs[0])) 54 | y = util.get_data(util.val_data_converted(ys[0])) 55 | 56 | if util.is_number(x) and util.is_number(y): 57 | return x - y 58 | 59 | if isinstance(x, nodes.FP_TimeBase) and isinstance(y, nodes.FP_Quantity): 60 | return x.plus(nodes.FP_Quantity(-y.value, y.unit)) 61 | 62 | if isinstance(x, str) and isinstance(y, nodes.FP_Quantity): 63 | x_ = nodes.FP_TimeBase.get_match_data(x) 64 | if x_ is not None: 65 | return x_.plus(nodes.FP_Quantity(-y.value, y.unit)) 66 | 67 | raise Exception("Cannot " + str(xs) + " - " + str(ys)) 68 | 69 | 70 | def mul(ctx, x, y): 71 | return x * y 72 | 73 | 74 | def div(ctx, x, y): 75 | if y == 0: 76 | return [] 77 | return x / y 78 | 79 | 80 | def intdiv(ctx, x, y): 81 | if y == 0: 82 | return [] 83 | return int(x / y) 84 | 85 | 86 | def mod(ctx, x, y): 87 | if y == 0: 88 | return [] 89 | 90 | return x % y 91 | 92 | 93 | # HACK: for only polymorphic function 94 | # Actually, "minus" is now also polymorphic 95 | def plus(ctx, xs_, ys_): 96 | xs = remove_duplicate_extension(xs_) 97 | ys = remove_duplicate_extension(ys_) 98 | 99 | if len(xs) != 1 or len(ys) != 1: 100 | raise Exception("Cannot " + str(xs) + " + " + str(ys)) 101 | 102 | x = util.get_data(util.val_data_converted(xs[0])) 103 | y = util.get_data(util.val_data_converted(ys[0])) 104 | 105 | """ 106 | In the future, this and other functions might need to return ResourceNode 107 | to preserve the type information (integer vs decimal, and maybe decimal 108 | vs string if decimals are represented as strings), in order to support 109 | "as" and "is", but that support is deferred for now. 110 | """ 111 | if isinstance(x, str) and isinstance(y, str): 112 | return x + y 113 | 114 | if util.is_number(x) and util.is_number(y): 115 | return x + y 116 | 117 | if isinstance(x, nodes.FP_TimeBase) and isinstance(y, nodes.FP_Quantity): 118 | return x.plus(y) 119 | 120 | if isinstance(x, str) and isinstance(y, nodes.FP_Quantity): 121 | x_ = nodes.FP_TimeBase.get_match_data(x) 122 | if x_ is not None: 123 | return x_.plus(y) 124 | 125 | raise Exception("Cannot " + str(xs) + " + " + str(ys)) 126 | 127 | 128 | def abs(ctx, x): 129 | if is_empty(x): 130 | return [] 131 | num = ensure_number_singleton(x) 132 | return Decimal(num).copy_abs() 133 | 134 | 135 | def ceiling(ctx, x): 136 | if is_empty(x): 137 | return [] 138 | num = ensure_number_singleton(x) 139 | return Decimal(num).to_integral_value(rounding="ROUND_CEILING") 140 | 141 | 142 | def exp(ctx, x): 143 | if is_empty(x): 144 | return [] 145 | num = ensure_number_singleton(x) 146 | return Decimal(num).exp() 147 | 148 | 149 | def floor(ctx, x): 150 | if is_empty(x): 151 | return [] 152 | num = ensure_number_singleton(x) 153 | return Decimal(num).to_integral_value(rounding="ROUND_FLOOR") 154 | 155 | 156 | def ln(ctx, x): 157 | if is_empty(x): 158 | return [] 159 | 160 | num = ensure_number_singleton(x) 161 | return Decimal(num).ln() 162 | 163 | 164 | def log(ctx, x, base): 165 | if is_empty(x) or is_empty(base): 166 | return [] 167 | 168 | num = Decimal(ensure_number_singleton(x)) 169 | num2 = Decimal(ensure_number_singleton(base)) 170 | 171 | return (num.ln() / num2.ln()).quantize(Decimal("1.000000000000000")) 172 | 173 | 174 | def power(ctx, x, degree): 175 | if is_empty(x) or is_empty(degree): 176 | return [] 177 | 178 | num = Decimal(ensure_number_singleton(x)) 179 | num2 = Decimal(ensure_number_singleton(degree)) 180 | 181 | if num < 0 or num2.to_integral_value(rounding="ROUND_FLOOR") != num2: 182 | return [] 183 | 184 | return pow(num, num2) 185 | 186 | 187 | def rround(ctx, x, acc): 188 | if is_empty(x): 189 | return [] 190 | 191 | num = Decimal(ensure_number_singleton(x)) 192 | if is_empty(acc): 193 | return round(num) 194 | 195 | num2 = ensure_number_singleton(acc) 196 | degree = 10 ** Decimal(num2) 197 | 198 | return round(num * degree) / degree 199 | 200 | 201 | def sqrt(ctx, x): 202 | if is_empty(x): 203 | return [] 204 | 205 | num = ensure_number_singleton(x) 206 | if num < 0: 207 | return [] 208 | 209 | return Decimal(num).sqrt() 210 | 211 | 212 | def truncate(ctx, x): 213 | if is_empty(x): 214 | return [] 215 | num = ensure_number_singleton(x) 216 | return Decimal(num).to_integral_value(rounding="ROUND_DOWN") 217 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/misc.py: -------------------------------------------------------------------------------- 1 | import re 2 | from decimal import Decimal 3 | 4 | import fhirpathpy.engine.util as util 5 | import fhirpathpy.engine.nodes as nodes 6 | 7 | # This file holds code to hande the FHIRPath Existence functions (5.1 in the 8 | # specification). 9 | 10 | intRegex = re.compile(r"^[+-]?\d+$") 11 | numRegex = re.compile(r"^[+-]?\d+(\.\d+)?$") 12 | 13 | 14 | def iif_macro(ctx, data, cond, ok, fail=None): 15 | if util.is_true(cond(data)): 16 | return ok(data) 17 | elif fail: 18 | return fail(data) 19 | else: 20 | return [] 21 | 22 | 23 | def trace_fn(ctx, x, label=""): 24 | print("TRACE:[" + label + "]", str(x)) 25 | return x 26 | 27 | 28 | def to_integer(ctx, coll): 29 | if len(coll) != 1: 30 | return [] 31 | 32 | value = util.get_data(coll[0]) 33 | 34 | if value == False: 35 | return 0 36 | 37 | if value == True: 38 | return 1 39 | 40 | if util.is_number(value): 41 | if int(value) == value: 42 | return value 43 | 44 | return [] 45 | 46 | if isinstance(value, str): 47 | if re.match(intRegex, value) is not None: 48 | return int(value) 49 | 50 | return [] 51 | 52 | 53 | quantity_regex = re.compile(r"^((\+|-)?\d+(\.\d+)?)\s*(('[^']+')|([a-zA-Z]+))?$") 54 | quantity_regex_map = {"value": 1, "unit": 5, "time": 6} 55 | 56 | 57 | def to_quantity(ctx, coll, to_unit=None): 58 | result = None 59 | 60 | # Surround UCUM unit code in the to_unit parameter with single quotes 61 | if to_unit and not nodes.FP_Quantity.timeUnitsToUCUM.get(to_unit): 62 | to_unit = f"'{to_unit}'" 63 | 64 | if len(coll) > 1: 65 | raise Exception("Could not convert to quantity: input collection contains multiple items") 66 | elif len(coll) == 1: 67 | v = util.val_data_converted(coll[0]) 68 | quantity_regex_res = None 69 | 70 | if isinstance(v, (int, Decimal)): 71 | result = nodes.FP_Quantity(v, "'1'") 72 | elif isinstance(v, nodes.FP_Quantity): 73 | result = v 74 | elif isinstance(v, bool): 75 | result = nodes.FP_Quantity(1 if v else 0, "'1'") 76 | elif isinstance(v, str): 77 | quantity_regex_res = quantity_regex.match(v) 78 | if quantity_regex_res: 79 | value = quantity_regex_res.group(quantity_regex_map["value"]) 80 | unit = quantity_regex_res.group(quantity_regex_map["unit"]) 81 | time = quantity_regex_res.group(quantity_regex_map["time"]) 82 | 83 | if not time or nodes.FP_Quantity.timeUnitsToUCUM.get(time): 84 | result = nodes.FP_Quantity(Decimal(value), unit or time or "'1'") 85 | 86 | if result and to_unit and result.unit != to_unit: 87 | result = nodes.FP_Quantity.conv_unit_to(result.unit, result.value, to_unit) 88 | 89 | return result if result else [] 90 | 91 | 92 | def to_decimal(ctx, coll): 93 | if len(coll) != 1: 94 | return [] 95 | 96 | value = util.get_data(coll[0]) 97 | 98 | if value is False: 99 | return Decimal(0) 100 | 101 | if value is True: 102 | return Decimal(1.0) 103 | 104 | if util.is_number(value): 105 | return Decimal(value) 106 | 107 | if isinstance(value, str): 108 | if re.match(numRegex, value) is not None: 109 | return Decimal(value) 110 | 111 | return [] 112 | 113 | 114 | def to_string(ctx, coll): 115 | if len(coll) != 1: 116 | return [] 117 | 118 | value = util.get_data(coll[0]) 119 | return str(value) 120 | 121 | 122 | # Defines a function on engine called to+timeType (e.g., toDateTime, etc.). 123 | # @param timeType The string name of a class for a time type (e.g. "FP_DateTime"). 124 | 125 | 126 | def to_date_time(ctx, coll): 127 | ln = len(coll) 128 | rtn = [] 129 | if ln > 1: 130 | raise Exception("to_date_time called for a collection of length " + str(ln)) 131 | 132 | if ln == 1: 133 | value = util.get_data(coll[0]) 134 | 135 | dateTimeObject = nodes.FP_DateTime(value) 136 | 137 | if dateTimeObject: 138 | rtn.append(dateTimeObject) 139 | 140 | return util.get_data(rtn[0]) 141 | 142 | 143 | def to_time(ctx, coll): 144 | ln = len(coll) 145 | rtn = [] 146 | if ln > 1: 147 | raise Exception("to_time called for a collection of length " + str(ln)) 148 | 149 | if ln == 1: 150 | value = util.get_data(coll[0]) 151 | 152 | timeObject = nodes.FP_Time(value) 153 | 154 | if timeObject: 155 | rtn.append(timeObject) 156 | 157 | return util.get_data(rtn[0]) 158 | 159 | 160 | def to_date(ctx, coll): 161 | ln = len(coll) 162 | rtn = [] 163 | 164 | if ln > 1: 165 | raise Exception("to_date called for a collection of length " + str(ln)) 166 | 167 | if ln == 1: 168 | value = util.get_data(coll[0]) 169 | 170 | dateObject = nodes.FP_DateTime(value) 171 | 172 | if dateObject: 173 | rtn.append(dateObject) 174 | 175 | return util.get_data(rtn[0]) 176 | 177 | 178 | def create_converts_to_fn(to_function, _type): 179 | if isinstance(_type, str): 180 | def in_function(ctx, coll): 181 | if len(coll) != 1: 182 | return [] 183 | return type(to_function(ctx, coll)).__name__ == _type 184 | return in_function 185 | 186 | def in_function(ctx, coll): 187 | if len(coll) != 1: 188 | return [] 189 | 190 | return isinstance(to_function(ctx, coll), _type) 191 | 192 | return in_function 193 | 194 | 195 | def to_boolean(ctx, coll): 196 | true_strings = ['true', 't', 'yes', 'y', '1', '1.0'] 197 | false_strings = ['false', 'f', 'no', 'n', '0', '0.0'] 198 | 199 | if len(coll) != 1: 200 | return [] 201 | 202 | val = coll[0] 203 | var_type = type(val).__name__ 204 | 205 | if var_type == "bool": 206 | return val 207 | elif var_type == "int" or var_type == "float": 208 | if val == 1 or val == 1.0: 209 | return True 210 | elif val == 0 or val == 0.0: 211 | return False 212 | elif var_type == "str": 213 | lower_case_var = val.lower() 214 | if lower_case_var in true_strings: 215 | return True 216 | if lower_case_var in false_strings: 217 | return False 218 | 219 | return [] 220 | 221 | 222 | def boolean_singleton(coll): 223 | d = util.get_data(coll[0]) 224 | if isinstance(d, bool): 225 | return d 226 | elif len(coll) == 1: 227 | return True 228 | 229 | def string_singleton(coll): 230 | d = util.get_data(coll[0]) 231 | if isinstance(d, str): 232 | return d 233 | 234 | singleton_eval_by_type = { 235 | "Boolean": boolean_singleton, 236 | "String": string_singleton, 237 | } 238 | 239 | def singleton(coll, type): 240 | if len(coll) > 1: 241 | raise Exception("Unexpected collection {coll}; expected singleton of type {type}".format(coll=coll, type=type)) 242 | elif len(coll) == 0: 243 | return [] 244 | to_singleton = singleton_eval_by_type[type] 245 | if to_singleton: 246 | val = to_singleton(coll) 247 | if (val is not None): 248 | return val 249 | raise Exception("Expected {type}, but got: {coll}".format(type=type.lower(), coll=coll)) 250 | raise Exception("Not supported type {}".format(type)) 251 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/navigation.py: -------------------------------------------------------------------------------- 1 | from collections import abc 2 | from functools import reduce 3 | import fhirpathpy.engine.util as util 4 | import fhirpathpy.engine.nodes as nodes 5 | 6 | create_node = nodes.ResourceNode.create_node 7 | 8 | 9 | def create_reduce_children(ctx): 10 | model = ctx["model"] 11 | 12 | def func(acc, res): 13 | data = util.get_data(res) 14 | res = create_node(res) 15 | 16 | if isinstance(data, list): 17 | data = dict((i, data[i]) for i in range(0, len(data))) 18 | 19 | if isinstance(data, abc.Mapping): 20 | for prop in data.keys(): 21 | value = data[prop] 22 | childPath = "" 23 | 24 | if res.path is not None: 25 | childPath = res.path + "." + prop 26 | 27 | if ( 28 | isinstance(model, dict) 29 | and "pathsDefinedElsewhere" in model 30 | and childPath in model["pathsDefinedElsewhere"] 31 | ): 32 | childPath = model["pathsDefinedElsewhere"][childPath] 33 | 34 | if isinstance(value, list): 35 | mapped = [create_node(n, childPath) for n in value] 36 | acc = acc + mapped 37 | else: 38 | acc.append(create_node(value, childPath)) 39 | return acc 40 | 41 | return func 42 | 43 | 44 | def children(ctx, coll): 45 | return reduce(create_reduce_children(ctx), coll, []) 46 | 47 | 48 | def descendants(ctx, coll): 49 | res = [] 50 | ch = children(ctx, coll) 51 | while len(ch) > 0: 52 | res = res + ch 53 | ch = children(ctx, ch) 54 | 55 | return res 56 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/strings.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | import fhirpathpy.engine.util as util 4 | 5 | 6 | def ensure_string_singleton(x): 7 | if len(x) == 1: 8 | d = util.get_data(x[0]) 9 | if type(d) == str: 10 | return d 11 | raise Exception("Expected string, but got " + str(d)) 12 | 13 | raise Exception("Expected string, but got " + str(x)) 14 | 15 | 16 | def index_of(ctx, coll, substr): 17 | string = ensure_string_singleton(coll) 18 | return string.find(substr) 19 | 20 | 21 | def substring(ctx, coll, start, length=None): 22 | string = ensure_string_singleton(coll) 23 | 24 | if isinstance(start, list) or start is None: 25 | return [] 26 | 27 | start = int(start) 28 | if start < 0 or start >= len(string): 29 | return [] 30 | 31 | if length is None or length == []: 32 | return string[start:] 33 | 34 | return string[start : start + int(length)] 35 | 36 | 37 | def starts_with(ctx, coll, prefix): 38 | if util.is_empty(prefix): 39 | return [] 40 | string = ensure_string_singleton(coll) 41 | if not string or not isinstance(prefix, str): 42 | return False 43 | return string.startswith(prefix) 44 | 45 | 46 | def ends_with(ctx, coll, postfix): 47 | if util.is_empty(postfix): 48 | return [] 49 | string = ensure_string_singleton(coll) 50 | if not string or not isinstance(postfix, str): 51 | return False 52 | return string.endswith(postfix) 53 | 54 | 55 | def contains_fn(ctx, coll, substr): 56 | string = ensure_string_singleton(coll) 57 | return substr in string 58 | 59 | 60 | def upper(ctx, coll): 61 | string = ensure_string_singleton(coll) 62 | return string.upper() 63 | 64 | 65 | def lower(ctx, coll): 66 | string = ensure_string_singleton(coll) 67 | return string.lower() 68 | 69 | 70 | def split(ctx, coll, delimiter): 71 | string = ensure_string_singleton(coll) 72 | return string.split(delimiter) 73 | 74 | 75 | def trim(ctx, coll): 76 | string = ensure_string_singleton(coll) 77 | return string.strip() 78 | 79 | 80 | def encode(ctx, coll, format): 81 | if not coll: 82 | return [] 83 | 84 | str_to_encode = coll[0] if isinstance(coll, list) else coll 85 | if not str_to_encode: 86 | return [] 87 | 88 | if format in ["urlbase64", "base64url"]: 89 | encoded = base64.b64encode(str_to_encode.encode()).decode() 90 | return encoded.replace("+", "-").replace("/", "_") 91 | 92 | if format == "base64": 93 | return base64.b64encode(str_to_encode.encode()).decode() 94 | 95 | if format == "hex": 96 | return "".join([hex(ord(c))[2:].zfill(2) for c in str_to_encode]) 97 | 98 | return [] 99 | 100 | 101 | def decode(ctx, coll, format): 102 | if not coll: 103 | return [] 104 | 105 | str_to_decode = coll[0] if isinstance(coll, list) else coll 106 | if not str_to_decode: 107 | return [] 108 | 109 | if format in ["urlbase64", "base64url"]: 110 | decoded = str_to_decode.replace("-", "+").replace("_", "/") 111 | return base64.b64decode(decoded).decode() 112 | 113 | if format == "base64": 114 | return base64.b64decode(str_to_decode).decode() 115 | 116 | if format == "hex": 117 | if len(str_to_decode) % 2 != 0: 118 | raise ValueError("Decode 'hex' requires an even number of characters.") 119 | return "".join( 120 | [chr(int(str_to_decode[i : i + 2], 16)) for i in range(0, len(str_to_decode), 2)] 121 | ) 122 | 123 | return [] 124 | 125 | 126 | def join(ctx, coll, separator=""): 127 | stringValues = [] 128 | for n in coll: 129 | d = util.get_data(n) 130 | if isinstance(d, str): 131 | stringValues.append(d) 132 | else: 133 | raise TypeError("Join requires a collection of strings.") 134 | 135 | return separator.join(stringValues) 136 | 137 | 138 | # test function 139 | def matches(ctx, coll, regex): 140 | if not regex or not coll: 141 | return [] 142 | 143 | string = ensure_string_singleton(coll) 144 | valid = re.compile(regex, re.DOTALL) 145 | return re.search(valid, string) is not None 146 | 147 | 148 | def replace(ctx, coll, regex, repl): 149 | string = ensure_string_singleton(coll) 150 | if regex == "" and isinstance(repl, str): 151 | return repl + repl.join(character for character in string) + repl 152 | if not string or not regex: 153 | return [] 154 | return string.replace(regex, repl) 155 | 156 | 157 | def replace_matches(ctx, coll, regex, repl): 158 | string = ensure_string_singleton(coll) 159 | if isinstance(regex, list) or isinstance(repl, list): 160 | return [] 161 | 162 | # extract capture groups $1, $2, $3 etc as in python \1, \2, \3 163 | repl = re.sub(r"\$(\d+)", r"\\\1", repl) 164 | 165 | valid = re.compile(regex) 166 | return re.sub(valid, repl, string) 167 | 168 | 169 | def length(ctx, coll): 170 | str = ensure_string_singleton(coll) 171 | return len(str) 172 | 173 | 174 | def toChars(ctx, coll): 175 | if not coll: 176 | return [] 177 | string = ensure_string_singleton(coll) 178 | return list(string) 179 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/subsetting.py: -------------------------------------------------------------------------------- 1 | def intersect_fn(ctx, list_1, list_2): 2 | intersection = [] 3 | 4 | for obj1 in list_1: 5 | for obj2 in list_2: 6 | if obj1 == obj2: 7 | intersection.append(obj1) 8 | break 9 | 10 | unique_intersection = [] 11 | for obj in intersection: 12 | if obj not in unique_intersection: 13 | unique_intersection.append(obj) 14 | 15 | return unique_intersection 16 | -------------------------------------------------------------------------------- /fhirpathpy/engine/invocations/types.py: -------------------------------------------------------------------------------- 1 | from fhirpathpy.engine.nodes import TypeInfo 2 | 3 | def type_fn(ctx, coll): 4 | return [TypeInfo.from_value(value).__dict__ for value in coll] 5 | 6 | 7 | def is_fn(ctx, coll, type_info): 8 | # TODO: It's incorrect place to setup model. Fix it. 9 | TypeInfo.model = ctx.get("model") 10 | if not coll: 11 | return [] 12 | if len(coll) > 1: 13 | raise ValueError(f"Expected singleton on left side of 'is', got {coll}") 14 | return TypeInfo.from_value(coll[0]).is_(type_info) 15 | 16 | 17 | def as_fn(ctx, coll, type_info): 18 | TypeInfo.model = ctx.get("model") 19 | if not coll: 20 | return [] 21 | if len(coll) > 1: 22 | raise ValueError(f"Expected singleton on left side of 'as', got {coll}") 23 | return coll if TypeInfo.from_value(coll[0]).is_(type_info) else [] 24 | -------------------------------------------------------------------------------- /fhirpathpy/engine/util.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | import json 3 | from collections import OrderedDict 4 | from functools import reduce 5 | from fhirpathpy.engine.nodes import ResourceNode, FP_Quantity 6 | 7 | 8 | class set_paths: 9 | def __init__(self, func, parsedPath, model=None, options=None): 10 | self.func = func 11 | self.parsedPath = parsedPath 12 | self.model = model 13 | self.options = options 14 | 15 | def __call__(self, resource, context=None): 16 | return self.func( 17 | resource, self.parsedPath, context or {}, self.model, self.options 18 | ) 19 | 20 | 21 | def get_data(value): 22 | if isinstance(value, ResourceNode): 23 | value = value.data 24 | 25 | if isinstance(value, float): 26 | return Decimal(str(value)) 27 | return value 28 | 29 | 30 | def parse_value(value): 31 | def parse_complex_value(v): 32 | num_value, unit = v.get("value"), v.get("code") 33 | return FP_Quantity(num_value, f"'{unit}'") if num_value and unit else None 34 | 35 | return ( 36 | parse_complex_value(value.data) 37 | if getattr(value, "get_type_info", lambda: None)() 38 | and value.get_type_info().name == "Quantity" 39 | else value 40 | ) 41 | 42 | 43 | def is_number(value): 44 | return isinstance(value, (int, Decimal, complex)) and not isinstance(value, bool) 45 | 46 | 47 | def is_capitalized(x): 48 | return isinstance(x, str) and x[0] == x[0].upper() 49 | 50 | 51 | def is_empty(x): 52 | return isinstance(x, list) and len(x) == 0 53 | 54 | 55 | def is_some(x): 56 | return x is not None and not is_empty(x) 57 | 58 | 59 | def is_nullable(x): 60 | return x is None or is_empty(x) 61 | 62 | 63 | def is_true(x): 64 | return x == True or isinstance(x, list) and len(x) == 1 and x[0] == True 65 | 66 | 67 | def arraify(x, instead_none=None): 68 | if isinstance(x, list): 69 | return x 70 | if is_some(x): 71 | return [x] 72 | return [] if instead_none is None else [instead_none] 73 | 74 | 75 | def flatten(x): 76 | def func(acc, x): 77 | if isinstance(x, list): 78 | acc = acc + x 79 | else: 80 | acc.append(x) 81 | 82 | return acc 83 | 84 | return reduce(func, x, []) 85 | 86 | 87 | def uniq(arr): 88 | # Strong type fast implementation for unique values that preserves ordering 89 | ordered_dict = OrderedDict() 90 | for x in arr: 91 | try: 92 | key = json.dumps(x, sort_keys=True) 93 | except TypeError: 94 | key = str(x) 95 | ordered_dict[key] = x 96 | return list(ordered_dict.values()) 97 | 98 | 99 | def val_data_converted(val): 100 | if isinstance(val, ResourceNode): 101 | val = val.convert_data() 102 | 103 | return val 104 | 105 | 106 | def process_user_invocation_table(table): 107 | return { 108 | name: { 109 | **entity, 110 | "fn": lambda ctx, inputs, *args, __fn__=entity["fn"]: __fn__( 111 | [get_data(i) for i in inputs], *args 112 | ), 113 | } 114 | for name, entity in table.items() 115 | } 116 | -------------------------------------------------------------------------------- /fhirpathpy/models/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import defaultdict 3 | import json 4 | 5 | import pathlib 6 | 7 | current_dir = pathlib.Path(__file__).parent.resolve() 8 | 9 | models = defaultdict(dict) 10 | 11 | dirs = [f for f in os.listdir(current_dir)] 12 | 13 | for d in dirs: 14 | pd = os.path.join(current_dir, d) 15 | if os.path.isdir(pd): 16 | for f in os.listdir(pd): 17 | with open(os.path.join(pd, f)) as fd: 18 | if f.endswith(".json"): 19 | models[d][f[:-5]] = json.loads(fd.read()) 20 | -------------------------------------------------------------------------------- /fhirpathpy/models/dstu2/pathsDefinedElsewhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "Bundle.entry.link": "Bundle.link", 3 | "Composition.section.section": "Composition.section", 4 | "ConceptMap.element.target.product": "ConceptMap.element.target.dependsOn", 5 | "Conformance.rest.searchParam": "Conformance.rest.resource.searchParam", 6 | "ImplementationGuide.page.page": "ImplementationGuide.page", 7 | "Observation.component.referenceRange": "Observation.referenceRange", 8 | "OperationDefinition.parameter.part": "OperationDefinition.parameter", 9 | "Parameters.parameter.part": "Parameters.parameter", 10 | "QuestionnaireResponse.group.group": "QuestionnaireResponse.group", 11 | "QuestionnaireResponse.group.question.answer.group": "QuestionnaireResponse.group", 12 | "TestScript.setup.metadata": "TestScript.metadata", 13 | "TestScript.teardown.action.operation": "TestScript.setup.action.operation", 14 | "TestScript.test.action.assert": "TestScript.setup.action.assert", 15 | "TestScript.test.action.operation": "TestScript.setup.action.operation", 16 | "TestScript.test.metadata": "TestScript.metadata", 17 | "ValueSet.codeSystem.concept.concept": "ValueSet.codeSystem.concept", 18 | "ValueSet.compose.exclude": "ValueSet.compose.include", 19 | "ValueSet.compose.include.concept.designation": "ValueSet.codeSystem.concept.designation", 20 | "ValueSet.expansion.contains.contains": "ValueSet.expansion.contains" 21 | } -------------------------------------------------------------------------------- /fhirpathpy/models/dstu2/type2Parent.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": "DomainResource", 3 | "Address": "Element", 4 | "Age": "Quantity", 5 | "AllergyIntolerance": "DomainResource", 6 | "Annotation": "Element", 7 | "Appointment": "DomainResource", 8 | "AppointmentResponse": "DomainResource", 9 | "Attachment": "Element", 10 | "AuditEvent": "DomainResource", 11 | "BackboneElement": "Element", 12 | "Basic": "DomainResource", 13 | "Binary": "Resource", 14 | "BodySite": "DomainResource", 15 | "Bundle": "Resource", 16 | "CarePlan": "DomainResource", 17 | "Claim": "DomainResource", 18 | "ClaimResponse": "DomainResource", 19 | "ClinicalImpression": "DomainResource", 20 | "CodeableConcept": "Element", 21 | "Coding": "Element", 22 | "Communication": "DomainResource", 23 | "CommunicationRequest": "DomainResource", 24 | "Composition": "DomainResource", 25 | "ConceptMap": "DomainResource", 26 | "Condition": "DomainResource", 27 | "Conformance": "DomainResource", 28 | "ContactPoint": "Element", 29 | "Contract": "DomainResource", 30 | "Count": "Quantity", 31 | "Coverage": "DomainResource", 32 | "DataElement": "DomainResource", 33 | "DetectedIssue": "DomainResource", 34 | "Device": "DomainResource", 35 | "DeviceComponent": "DomainResource", 36 | "DeviceMetric": "DomainResource", 37 | "DeviceUseRequest": "DomainResource", 38 | "DeviceUseStatement": "DomainResource", 39 | "DiagnosticOrder": "DomainResource", 40 | "DiagnosticReport": "DomainResource", 41 | "Distance": "Quantity", 42 | "DocumentManifest": "DomainResource", 43 | "DocumentReference": "DomainResource", 44 | "DomainResource": "Resource", 45 | "Duration": "Quantity", 46 | "ElementDefinition": "Element", 47 | "EligibilityRequest": "DomainResource", 48 | "EligibilityResponse": "DomainResource", 49 | "Encounter": "DomainResource", 50 | "EnrollmentRequest": "DomainResource", 51 | "EnrollmentResponse": "DomainResource", 52 | "EpisodeOfCare": "DomainResource", 53 | "ExplanationOfBenefit": "DomainResource", 54 | "Extension": "Element", 55 | "FamilyMemberHistory": "DomainResource", 56 | "Flag": "DomainResource", 57 | "Goal": "DomainResource", 58 | "Group": "DomainResource", 59 | "HealthcareService": "DomainResource", 60 | "HumanName": "Element", 61 | "Identifier": "Element", 62 | "ImagingObjectSelection": "DomainResource", 63 | "ImagingStudy": "DomainResource", 64 | "Immunization": "DomainResource", 65 | "ImmunizationRecommendation": "DomainResource", 66 | "ImplementationGuide": "DomainResource", 67 | "List": "DomainResource", 68 | "Location": "DomainResource", 69 | "Media": "DomainResource", 70 | "Medication": "DomainResource", 71 | "MedicationAdministration": "DomainResource", 72 | "MedicationDispense": "DomainResource", 73 | "MedicationOrder": "DomainResource", 74 | "MedicationStatement": "DomainResource", 75 | "MessageHeader": "DomainResource", 76 | "Meta": "Element", 77 | "Money": "Quantity", 78 | "NamingSystem": "DomainResource", 79 | "Narrative": "Element", 80 | "NutritionOrder": "DomainResource", 81 | "Observation": "DomainResource", 82 | "OperationDefinition": "DomainResource", 83 | "OperationOutcome": "DomainResource", 84 | "Order": "DomainResource", 85 | "OrderResponse": "DomainResource", 86 | "Organization": "DomainResource", 87 | "Parameters": "Resource", 88 | "Patient": "DomainResource", 89 | "PaymentNotice": "DomainResource", 90 | "PaymentReconciliation": "DomainResource", 91 | "Period": "Element", 92 | "Person": "DomainResource", 93 | "Practitioner": "DomainResource", 94 | "Procedure": "DomainResource", 95 | "ProcedureRequest": "DomainResource", 96 | "ProcessRequest": "DomainResource", 97 | "ProcessResponse": "DomainResource", 98 | "Provenance": "DomainResource", 99 | "Quantity": "Element", 100 | "Questionnaire": "DomainResource", 101 | "QuestionnaireResponse": "DomainResource", 102 | "Range": "Element", 103 | "Ratio": "Element", 104 | "Reference": "Element", 105 | "ReferralRequest": "DomainResource", 106 | "RelatedPerson": "DomainResource", 107 | "RiskAssessment": "DomainResource", 108 | "SampledData": "Element", 109 | "Schedule": "DomainResource", 110 | "SearchParameter": "DomainResource", 111 | "Signature": "Element", 112 | "SimpleQuantity": "Quantity", 113 | "Slot": "DomainResource", 114 | "Specimen": "DomainResource", 115 | "StructureDefinition": "DomainResource", 116 | "Subscription": "DomainResource", 117 | "Substance": "DomainResource", 118 | "SupplyDelivery": "DomainResource", 119 | "SupplyRequest": "DomainResource", 120 | "TestScript": "DomainResource", 121 | "Timing": "Element", 122 | "ValueSet": "DomainResource", 123 | "VisionPrescription": "DomainResource", 124 | "base64Binary": "Element", 125 | "boolean": "Element", 126 | "code": "string", 127 | "date": "Element", 128 | "dateTime": "Element", 129 | "decimal": "Element", 130 | "id": "string", 131 | "instant": "Element", 132 | "integer": "Element", 133 | "markdown": "string", 134 | "oid": "uri", 135 | "positiveInt": "integer", 136 | "string": "Element", 137 | "time": "Element", 138 | "unsignedInt": "integer", 139 | "uri": "Element", 140 | "uuid": "uri" 141 | } -------------------------------------------------------------------------------- /fhirpathpy/models/r4/pathsDefinedElsewhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "Bundle.entry.link": "Bundle.link", 3 | "CapabilityStatement.rest.operation": "CapabilityStatement.rest.resource.operation", 4 | "CapabilityStatement.rest.searchParam": "CapabilityStatement.rest.resource.searchParam", 5 | "ChargeItemDefinition.propertyGroup.applicability": "ChargeItemDefinition.applicability", 6 | "ClaimResponse.addItem.adjudication": "ClaimResponse.item.adjudication", 7 | "ClaimResponse.addItem.detail.adjudication": "ClaimResponse.item.adjudication", 8 | "ClaimResponse.addItem.detail.subDetail.adjudication": "ClaimResponse.item.adjudication", 9 | "ClaimResponse.adjudication": "ClaimResponse.item.adjudication", 10 | "ClaimResponse.item.detail.adjudication": "ClaimResponse.item.adjudication", 11 | "ClaimResponse.item.detail.subDetail.adjudication": "ClaimResponse.item.adjudication", 12 | "CodeSystem.concept.concept": "CodeSystem.concept", 13 | "Composition.section.section": "Composition.section", 14 | "ConceptMap.group.element.target.product": "ConceptMap.group.element.target.dependsOn", 15 | "Consent.provision.provision": "Consent.provision", 16 | "Contract.term.asset.answer": "Contract.term.offer.answer", 17 | "Contract.term.group": "Contract.term", 18 | "ExampleScenario.process.step.alternative.step": "ExampleScenario.process.step", 19 | "ExampleScenario.process.step.operation.request": "ExampleScenario.instance.containedInstance", 20 | "ExampleScenario.process.step.operation.response": "ExampleScenario.instance.containedInstance", 21 | "ExampleScenario.process.step.process": "ExampleScenario.process", 22 | "ExplanationOfBenefit.addItem.adjudication": "ExplanationOfBenefit.item.adjudication", 23 | "ExplanationOfBenefit.addItem.detail.adjudication": "ExplanationOfBenefit.item.adjudication", 24 | "ExplanationOfBenefit.addItem.detail.subDetail.adjudication": "ExplanationOfBenefit.item.adjudication", 25 | "ExplanationOfBenefit.adjudication": "ExplanationOfBenefit.item.adjudication", 26 | "ExplanationOfBenefit.item.detail.adjudication": "ExplanationOfBenefit.item.adjudication", 27 | "ExplanationOfBenefit.item.detail.subDetail.adjudication": "ExplanationOfBenefit.item.adjudication", 28 | "GraphDefinition.link.target.link": "GraphDefinition.link", 29 | "ImplementationGuide.definition.page.page": "ImplementationGuide.definition.page", 30 | "Invoice.totalPriceComponent": "Invoice.lineItem.priceComponent", 31 | "MedicinalProductAuthorization.procedure.application": "MedicinalProductAuthorization.procedure", 32 | "MedicinalProductIngredient.substance.strength": "MedicinalProductIngredient.specifiedSubstance.strength", 33 | "MedicinalProductPackaged.packageItem.packageItem": "MedicinalProductPackaged.packageItem", 34 | "Observation.component.referenceRange": "Observation.referenceRange", 35 | "OperationDefinition.parameter.part": "OperationDefinition.parameter", 36 | "Parameters.parameter.part": "Parameters.parameter", 37 | "PlanDefinition.action.action": "PlanDefinition.action", 38 | "Provenance.entity.agent": "Provenance.agent", 39 | "Questionnaire.item.item": "Questionnaire.item", 40 | "QuestionnaireResponse.item.answer.item": "QuestionnaireResponse.item", 41 | "QuestionnaireResponse.item.item": "QuestionnaireResponse.item", 42 | "RequestGroup.action.action": "RequestGroup.action", 43 | "StructureMap.group.rule.rule": "StructureMap.group.rule", 44 | "SubstanceSpecification.molecularWeight": "SubstanceSpecification.structure.isotope.molecularWeight", 45 | "SubstanceSpecification.name.synonym": "SubstanceSpecification.name", 46 | "SubstanceSpecification.name.translation": "SubstanceSpecification.name", 47 | "SubstanceSpecification.structure.molecularWeight": "SubstanceSpecification.structure.isotope.molecularWeight", 48 | "TestReport.teardown.action.operation": "TestReport.setup.action.operation", 49 | "TestReport.test.action.assert": "TestReport.setup.action.assert", 50 | "TestReport.test.action.operation": "TestReport.setup.action.operation", 51 | "TestScript.teardown.action.operation": "TestScript.setup.action.operation", 52 | "TestScript.test.action.assert": "TestScript.setup.action.assert", 53 | "TestScript.test.action.operation": "TestScript.setup.action.operation", 54 | "ValueSet.compose.exclude": "ValueSet.compose.include", 55 | "ValueSet.expansion.contains.contains": "ValueSet.expansion.contains", 56 | "ValueSet.expansion.contains.designation": "ValueSet.compose.include.concept.designation" 57 | } -------------------------------------------------------------------------------- /fhirpathpy/models/r5/pathsDefinedElsewhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "ArtifactAssessment.content.component": "ArtifactAssessment.content", 3 | "AuditEvent.entity.agent": "AuditEvent.agent", 4 | "BodyStructure.excludedStructure": "BodyStructure.includedStructure", 5 | "Bundle.entry.link": "Bundle.link", 6 | "CapabilityStatement.rest.operation": "CapabilityStatement.rest.resource.operation", 7 | "CapabilityStatement.rest.searchParam": "CapabilityStatement.rest.resource.searchParam", 8 | "ChargeItemDefinition.propertyGroup.applicability": "ChargeItemDefinition.applicability", 9 | "ClaimResponse.addItem.adjudication": "ClaimResponse.item.adjudication", 10 | "ClaimResponse.addItem.detail.adjudication": "ClaimResponse.item.adjudication", 11 | "ClaimResponse.addItem.detail.reviewOutcome": "ClaimResponse.item.reviewOutcome", 12 | "ClaimResponse.addItem.detail.subDetail.adjudication": "ClaimResponse.item.adjudication", 13 | "ClaimResponse.addItem.detail.subDetail.reviewOutcome": "ClaimResponse.item.reviewOutcome", 14 | "ClaimResponse.addItem.reviewOutcome": "ClaimResponse.item.reviewOutcome", 15 | "ClaimResponse.adjudication": "ClaimResponse.item.adjudication", 16 | "ClaimResponse.item.detail.adjudication": "ClaimResponse.item.adjudication", 17 | "ClaimResponse.item.detail.reviewOutcome": "ClaimResponse.item.reviewOutcome", 18 | "ClaimResponse.item.detail.subDetail.adjudication": "ClaimResponse.item.adjudication", 19 | "ClaimResponse.item.detail.subDetail.reviewOutcome": "ClaimResponse.item.reviewOutcome", 20 | "ClinicalUseDefinition.indication.otherTherapy": "ClinicalUseDefinition.contraindication.otherTherapy", 21 | "CodeSystem.concept.concept": "CodeSystem.concept", 22 | "Composition.section.section": "Composition.section", 23 | "ConceptMap.group.element.target.product": "ConceptMap.group.element.target.dependsOn", 24 | "Consent.provision.provision": "Consent.provision", 25 | "Contract.term.asset.answer": "Contract.term.offer.answer", 26 | "Contract.term.group": "Contract.term", 27 | "DeviceDefinition.packaging.packaging": "DeviceDefinition.packaging", 28 | "DeviceDefinition.packaging.udiDeviceIdentifier": "DeviceDefinition.udiDeviceIdentifier", 29 | "Evidence.certainty.subcomponent": "Evidence.certainty", 30 | "Evidence.statistic.attributeEstimate.attributeEstimate": "Evidence.statistic.attributeEstimate", 31 | "Evidence.statistic.modelCharacteristic.attributeEstimate": "Evidence.statistic.attributeEstimate", 32 | "EvidenceReport.section.section": "EvidenceReport.section", 33 | "EvidenceVariable.characteristic.definitionByCombination.characteristic": "EvidenceVariable.characteristic", 34 | "ExampleScenario.process.step.alternative.step": "ExampleScenario.process.step", 35 | "ExampleScenario.process.step.operation.request": "ExampleScenario.instance.containedInstance", 36 | "ExampleScenario.process.step.operation.response": "ExampleScenario.instance.containedInstance", 37 | "ExampleScenario.process.step.process": "ExampleScenario.process", 38 | "ExplanationOfBenefit.addItem.adjudication": "ExplanationOfBenefit.item.adjudication", 39 | "ExplanationOfBenefit.addItem.detail.adjudication": "ExplanationOfBenefit.item.adjudication", 40 | "ExplanationOfBenefit.addItem.detail.reviewOutcome": "ExplanationOfBenefit.item.reviewOutcome", 41 | "ExplanationOfBenefit.addItem.detail.subDetail.adjudication": "ExplanationOfBenefit.item.adjudication", 42 | "ExplanationOfBenefit.addItem.detail.subDetail.reviewOutcome": "ExplanationOfBenefit.item.reviewOutcome", 43 | "ExplanationOfBenefit.addItem.reviewOutcome": "ExplanationOfBenefit.item.reviewOutcome", 44 | "ExplanationOfBenefit.adjudication": "ExplanationOfBenefit.item.adjudication", 45 | "ExplanationOfBenefit.item.detail.adjudication": "ExplanationOfBenefit.item.adjudication", 46 | "ExplanationOfBenefit.item.detail.reviewOutcome": "ExplanationOfBenefit.item.reviewOutcome", 47 | "ExplanationOfBenefit.item.detail.subDetail.adjudication": "ExplanationOfBenefit.item.adjudication", 48 | "ExplanationOfBenefit.item.detail.subDetail.reviewOutcome": "ExplanationOfBenefit.item.reviewOutcome", 49 | "ImplementationGuide.definition.page.page": "ImplementationGuide.definition.page", 50 | "ManufacturedItemDefinition.component.component": "ManufacturedItemDefinition.component", 51 | "ManufacturedItemDefinition.component.property": "ManufacturedItemDefinition.property", 52 | "MedicationKnowledge.packaging.cost": "MedicationKnowledge.cost", 53 | "Observation.component.referenceRange": "Observation.referenceRange", 54 | "ObservationDefinition.component.qualifiedValue": "ObservationDefinition.qualifiedValue", 55 | "OperationDefinition.parameter.part": "OperationDefinition.parameter", 56 | "PackagedProductDefinition.characteristic": "PackagedProductDefinition.packaging.property", 57 | "PackagedProductDefinition.packaging.packaging": "PackagedProductDefinition.packaging", 58 | "Parameters.parameter.part": "Parameters.parameter", 59 | "PlanDefinition.action.action": "PlanDefinition.action", 60 | "Provenance.entity.agent": "Provenance.agent", 61 | "Questionnaire.item.item": "Questionnaire.item", 62 | "QuestionnaireResponse.item.answer.item": "QuestionnaireResponse.item", 63 | "QuestionnaireResponse.item.item": "QuestionnaireResponse.item", 64 | "RegulatedAuthorization.case.application": "RegulatedAuthorization.case", 65 | "RequestOrchestration.action.action": "RequestOrchestration.action", 66 | "StructureMap.group.rule.dependent.parameter": "StructureMap.group.rule.target.parameter", 67 | "StructureMap.group.rule.rule": "StructureMap.group.rule", 68 | "SubstanceDefinition.name.synonym": "SubstanceDefinition.name", 69 | "SubstanceDefinition.name.translation": "SubstanceDefinition.name", 70 | "SubstanceDefinition.structure.molecularWeight": "SubstanceDefinition.molecularWeight", 71 | "TestReport.teardown.action.operation": "TestReport.setup.action.operation", 72 | "TestReport.test.action.assert": "TestReport.setup.action.assert", 73 | "TestReport.test.action.operation": "TestReport.setup.action.operation", 74 | "TestScript.teardown.action.operation": "TestScript.setup.action.operation", 75 | "TestScript.test.action.assert": "TestScript.setup.action.assert", 76 | "TestScript.test.action.operation": "TestScript.setup.action.operation", 77 | "ValueSet.compose.exclude": "ValueSet.compose.include", 78 | "ValueSet.expansion.contains.contains": "ValueSet.expansion.contains", 79 | "ValueSet.expansion.contains.designation": "ValueSet.compose.include.concept.designation" 80 | } -------------------------------------------------------------------------------- /fhirpathpy/models/stu3/pathsDefinedElsewhere.json: -------------------------------------------------------------------------------- 1 | { 2 | "Bundle.entry.link": "Bundle.link", 3 | "CapabilityStatement.rest.searchParam": "CapabilityStatement.rest.resource.searchParam", 4 | "ClaimResponse.addItem.adjudication": "ClaimResponse.item.adjudication", 5 | "ClaimResponse.addItem.detail.adjudication": "ClaimResponse.item.adjudication", 6 | "ClaimResponse.item.detail.adjudication": "ClaimResponse.item.adjudication", 7 | "ClaimResponse.item.detail.subDetail.adjudication": "ClaimResponse.item.adjudication", 8 | "CodeSystem.concept.concept": "CodeSystem.concept", 9 | "Composition.section.section": "Composition.section", 10 | "ConceptMap.group.element.target.product": "ConceptMap.group.element.target.dependsOn", 11 | "Contract.term.group": "Contract.term", 12 | "ExplanationOfBenefit.addItem.adjudication": "ExplanationOfBenefit.item.adjudication", 13 | "ExplanationOfBenefit.addItem.detail.adjudication": "ExplanationOfBenefit.item.adjudication", 14 | "ExplanationOfBenefit.item.detail.adjudication": "ExplanationOfBenefit.item.adjudication", 15 | "ExplanationOfBenefit.item.detail.subDetail.adjudication": "ExplanationOfBenefit.item.adjudication", 16 | "GraphDefinition.link.target.link": "GraphDefinition.link", 17 | "ImplementationGuide.page.page": "ImplementationGuide.page", 18 | "Observation.component.referenceRange": "Observation.referenceRange", 19 | "OperationDefinition.parameter.part": "OperationDefinition.parameter", 20 | "Parameters.parameter.part": "Parameters.parameter", 21 | "PlanDefinition.action.action": "PlanDefinition.action", 22 | "Provenance.entity.agent": "Provenance.agent", 23 | "Questionnaire.item.item": "Questionnaire.item", 24 | "QuestionnaireResponse.item.answer.item": "QuestionnaireResponse.item", 25 | "QuestionnaireResponse.item.item": "QuestionnaireResponse.item", 26 | "RequestGroup.action.action": "RequestGroup.action", 27 | "StructureMap.group.rule.rule": "StructureMap.group.rule", 28 | "TestReport.teardown.action.operation": "TestReport.setup.action.operation", 29 | "TestReport.test.action.assert": "TestReport.setup.action.assert", 30 | "TestReport.test.action.operation": "TestReport.setup.action.operation", 31 | "TestScript.teardown.action.operation": "TestScript.setup.action.operation", 32 | "TestScript.test.action.assert": "TestScript.setup.action.assert", 33 | "TestScript.test.action.operation": "TestScript.setup.action.operation", 34 | "ValueSet.compose.exclude": "ValueSet.compose.include", 35 | "ValueSet.expansion.contains.contains": "ValueSet.expansion.contains", 36 | "ValueSet.expansion.contains.designation": "ValueSet.compose.include.concept.designation" 37 | } -------------------------------------------------------------------------------- /fhirpathpy/models/stu3/type2Parent.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": "DomainResource", 3 | "ActivityDefinition": "DomainResource", 4 | "Address": "Element", 5 | "AdverseEvent": "DomainResource", 6 | "Age": "Quantity", 7 | "AllergyIntolerance": "DomainResource", 8 | "Annotation": "Element", 9 | "Appointment": "DomainResource", 10 | "AppointmentResponse": "DomainResource", 11 | "Attachment": "Element", 12 | "AuditEvent": "DomainResource", 13 | "BackboneElement": "Element", 14 | "Basic": "DomainResource", 15 | "Binary": "Resource", 16 | "BodySite": "DomainResource", 17 | "Bundle": "Resource", 18 | "CapabilityStatement": "DomainResource", 19 | "CarePlan": "DomainResource", 20 | "CareTeam": "DomainResource", 21 | "ChargeItem": "DomainResource", 22 | "Claim": "DomainResource", 23 | "ClaimResponse": "DomainResource", 24 | "ClinicalImpression": "DomainResource", 25 | "CodeSystem": "DomainResource", 26 | "CodeableConcept": "Element", 27 | "Coding": "Element", 28 | "Communication": "DomainResource", 29 | "CommunicationRequest": "DomainResource", 30 | "CompartmentDefinition": "DomainResource", 31 | "Composition": "DomainResource", 32 | "ConceptMap": "DomainResource", 33 | "Condition": "DomainResource", 34 | "Consent": "DomainResource", 35 | "ContactDetail": "Element", 36 | "ContactPoint": "Element", 37 | "Contract": "DomainResource", 38 | "Contributor": "Element", 39 | "Count": "Quantity", 40 | "Coverage": "DomainResource", 41 | "DataElement": "DomainResource", 42 | "DataRequirement": "Element", 43 | "DetectedIssue": "DomainResource", 44 | "Device": "DomainResource", 45 | "DeviceComponent": "DomainResource", 46 | "DeviceMetric": "DomainResource", 47 | "DeviceRequest": "DomainResource", 48 | "DeviceUseStatement": "DomainResource", 49 | "DiagnosticReport": "DomainResource", 50 | "Distance": "Quantity", 51 | "DocumentManifest": "DomainResource", 52 | "DocumentReference": "DomainResource", 53 | "DomainResource": "Resource", 54 | "Dosage": "Element", 55 | "Duration": "Quantity", 56 | "ElementDefinition": "Element", 57 | "EligibilityRequest": "DomainResource", 58 | "EligibilityResponse": "DomainResource", 59 | "Encounter": "DomainResource", 60 | "Endpoint": "DomainResource", 61 | "EnrollmentRequest": "DomainResource", 62 | "EnrollmentResponse": "DomainResource", 63 | "EpisodeOfCare": "DomainResource", 64 | "ExpansionProfile": "DomainResource", 65 | "ExplanationOfBenefit": "DomainResource", 66 | "Extension": "Element", 67 | "FamilyMemberHistory": "DomainResource", 68 | "Flag": "DomainResource", 69 | "Goal": "DomainResource", 70 | "GraphDefinition": "DomainResource", 71 | "Group": "DomainResource", 72 | "GuidanceResponse": "DomainResource", 73 | "HealthcareService": "DomainResource", 74 | "HumanName": "Element", 75 | "Identifier": "Element", 76 | "ImagingManifest": "DomainResource", 77 | "ImagingStudy": "DomainResource", 78 | "Immunization": "DomainResource", 79 | "ImmunizationRecommendation": "DomainResource", 80 | "ImplementationGuide": "DomainResource", 81 | "Library": "DomainResource", 82 | "Linkage": "DomainResource", 83 | "List": "DomainResource", 84 | "Location": "DomainResource", 85 | "Measure": "DomainResource", 86 | "MeasureReport": "DomainResource", 87 | "Media": "DomainResource", 88 | "Medication": "DomainResource", 89 | "MedicationAdministration": "DomainResource", 90 | "MedicationDispense": "DomainResource", 91 | "MedicationRequest": "DomainResource", 92 | "MedicationStatement": "DomainResource", 93 | "MessageDefinition": "DomainResource", 94 | "MessageHeader": "DomainResource", 95 | "Meta": "Element", 96 | "Money": "Quantity", 97 | "NamingSystem": "DomainResource", 98 | "Narrative": "Element", 99 | "NutritionOrder": "DomainResource", 100 | "Observation": "DomainResource", 101 | "OperationDefinition": "DomainResource", 102 | "OperationOutcome": "DomainResource", 103 | "Organization": "DomainResource", 104 | "ParameterDefinition": "Element", 105 | "Parameters": "Resource", 106 | "Patient": "DomainResource", 107 | "PaymentNotice": "DomainResource", 108 | "PaymentReconciliation": "DomainResource", 109 | "Period": "Element", 110 | "Person": "DomainResource", 111 | "PlanDefinition": "DomainResource", 112 | "Practitioner": "DomainResource", 113 | "PractitionerRole": "DomainResource", 114 | "Procedure": "DomainResource", 115 | "ProcedureRequest": "DomainResource", 116 | "ProcessRequest": "DomainResource", 117 | "ProcessResponse": "DomainResource", 118 | "Provenance": "DomainResource", 119 | "Quantity": "Element", 120 | "Questionnaire": "DomainResource", 121 | "QuestionnaireResponse": "DomainResource", 122 | "Range": "Element", 123 | "Ratio": "Element", 124 | "Reference": "Element", 125 | "ReferralRequest": "DomainResource", 126 | "RelatedArtifact": "Element", 127 | "RelatedPerson": "DomainResource", 128 | "RequestGroup": "DomainResource", 129 | "ResearchStudy": "DomainResource", 130 | "ResearchSubject": "DomainResource", 131 | "RiskAssessment": "DomainResource", 132 | "SampledData": "Element", 133 | "Schedule": "DomainResource", 134 | "SearchParameter": "DomainResource", 135 | "Sequence": "DomainResource", 136 | "ServiceDefinition": "DomainResource", 137 | "Signature": "Element", 138 | "SimpleQuantity": "Quantity", 139 | "Slot": "DomainResource", 140 | "Specimen": "DomainResource", 141 | "StructureDefinition": "DomainResource", 142 | "StructureMap": "DomainResource", 143 | "Subscription": "DomainResource", 144 | "Substance": "DomainResource", 145 | "SupplyDelivery": "DomainResource", 146 | "SupplyRequest": "DomainResource", 147 | "Task": "DomainResource", 148 | "TestReport": "DomainResource", 149 | "TestScript": "DomainResource", 150 | "Timing": "Element", 151 | "TriggerDefinition": "Element", 152 | "UsageContext": "Element", 153 | "ValueSet": "DomainResource", 154 | "VisionPrescription": "DomainResource", 155 | "base64Binary": "Element", 156 | "boolean": "Element", 157 | "code": "string", 158 | "date": "Element", 159 | "dateTime": "Element", 160 | "decimal": "Element", 161 | "id": "string", 162 | "instant": "Element", 163 | "integer": "Element", 164 | "markdown": "string", 165 | "oid": "uri", 166 | "positiveInt": "integer", 167 | "string": "Element", 168 | "time": "Element", 169 | "unsignedInt": "integer", 170 | "uri": "Element", 171 | "uuid": "uri", 172 | "xhtml": "Element" 173 | } -------------------------------------------------------------------------------- /fhirpathpy/parser/ASTPathListener.py: -------------------------------------------------------------------------------- 1 | from antlr4.tree.Tree import TerminalNodeImpl 2 | from fhirpathpy.parser.generated.FHIRPathListener import FHIRPathListener 3 | 4 | 5 | def has_node_type_text(node_type): 6 | # In general we need mostly terminal nodes (e.g. Identifier and any Literal) 7 | # But the code also uses TypeSpecifier, InvocationExpression and TermExpression 8 | return node_type.endswith("Literal") or node_type in [ 9 | "LiteralTerm", 10 | "Identifier", 11 | "TypeSpecifier", 12 | "InvocationExpression", 13 | "TermExpression", 14 | ] 15 | 16 | 17 | class ASTPathListener(FHIRPathListener): 18 | def __init__(self): 19 | self.parentStack = [{}] 20 | 21 | def pushNode(self, nodeType, ctx): 22 | parentNode = self.parentStack[-1] 23 | node = {"type": nodeType, "terminalNodeText": []} 24 | if has_node_type_text(nodeType): 25 | node["text"] = ctx.getText() 26 | for child in ctx.children: 27 | if isinstance(child, TerminalNodeImpl): 28 | node["terminalNodeText"].append(child.getText()) 29 | 30 | if "children" not in parentNode: 31 | parentNode["children"] = [] 32 | 33 | parentNode["children"].append(node) 34 | 35 | self.parentStack.append(node) 36 | 37 | def popNode(self): 38 | if len(self.parentStack) > 0: 39 | self.parentStack.pop() 40 | 41 | def __getattribute__(self, name): 42 | attr = object.__getattribute__(self, name) 43 | 44 | if name in FHIRPathListener.__dict__ and callable(attr): 45 | 46 | def newfunc(*args, **kwargs): 47 | if name.startswith("enter"): 48 | self.pushNode(name[5:], args[0]) 49 | 50 | if name.startswith("exit"): 51 | self.popNode() 52 | 53 | return attr(*args, **kwargs) 54 | 55 | return newfunc 56 | return attr 57 | -------------------------------------------------------------------------------- /fhirpathpy/parser/FHIRPath.g4: -------------------------------------------------------------------------------- 1 | grammar FHIRPath; 2 | 3 | // Grammar rules 4 | // [FHIRPath](http://hl7.org/fhirpath/N1) Normative Release 5 | 6 | //prog: line (line)*; 7 | //line: ID ( '(' expr ')') ':' expr '\r'? '\n'; 8 | entireExpression 9 | : expression EOF 10 | ; 11 | 12 | expression 13 | : term #termExpression 14 | | expression '.' invocation #invocationExpression 15 | | expression '[' expression ']' #indexerExpression 16 | | ('+' | '-') expression #polarityExpression 17 | | expression ('*' | '/' | 'div' | 'mod') expression #multiplicativeExpression 18 | | expression ('+' | '-' | '&') expression #additiveExpression 19 | | expression '|' expression #unionExpression 20 | | expression ('<=' | '<' | '>' | '>=') expression #inequalityExpression 21 | | expression ('is' | 'as') typeSpecifier #typeExpression 22 | | expression ('=' | '~' | '!=' | '!~') expression #equalityExpression 23 | | expression ('in' | 'contains') expression #membershipExpression 24 | | expression 'and' expression #andExpression 25 | | expression ('or' | 'xor') expression #orExpression 26 | | expression 'implies' expression #impliesExpression 27 | //| (IDENTIFIER)? '=>' expression #lambdaExpression 28 | ; 29 | 30 | term 31 | : invocation #invocationTerm 32 | | literal #literalTerm 33 | | externalConstant #externalConstantTerm 34 | | '(' expression ')' #parenthesizedTerm 35 | ; 36 | 37 | literal 38 | : '{' '}' #nullLiteral 39 | | ('true' | 'false') #booleanLiteral 40 | | STRING #stringLiteral 41 | | NUMBER #numberLiteral 42 | | DATETIME #dateTimeLiteral 43 | | TIME #timeLiteral 44 | | quantity #quantityLiteral 45 | ; 46 | 47 | externalConstant 48 | : '%' ( identifier | STRING ) 49 | ; 50 | 51 | invocation // Terms that can be used after the function/member invocation '.' 52 | : identifier #memberInvocation 53 | | functn #functionInvocation 54 | | '$this' #thisInvocation 55 | | '$index' #indexInvocation 56 | | '$total' #totalInvocation 57 | ; 58 | 59 | functn 60 | : identifier '(' paramList? ')' 61 | ; 62 | 63 | paramList 64 | : expression (',' expression)* 65 | ; 66 | 67 | quantity 68 | : NUMBER unit? 69 | ; 70 | 71 | unit 72 | : dateTimePrecision 73 | | pluralDateTimePrecision 74 | | STRING // UCUM syntax for units of measure 75 | ; 76 | 77 | dateTimePrecision 78 | : 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond' 79 | ; 80 | 81 | pluralDateTimePrecision 82 | : 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds' 83 | ; 84 | 85 | typeSpecifier 86 | : qualifiedIdentifier 87 | ; 88 | 89 | qualifiedIdentifier 90 | : identifier ('.' identifier)* 91 | ; 92 | 93 | identifier 94 | : IDENTIFIER 95 | | DELIMITEDIDENTIFIER 96 | | 'as' 97 | | 'is' 98 | | 'contains' 99 | | 'in' 100 | ; 101 | 102 | 103 | /**************************************************************** 104 | Lexical rules 105 | *****************************************************************/ 106 | 107 | /* 108 | NOTE: The goal of these rules in the grammar is to provide a date 109 | token to the parser. As such it is not attempting to validate that 110 | the date is a correct date, that task is for the parser or interpreter. 111 | */ 112 | 113 | DATETIME 114 | : '@' 115 | [0-9][0-9][0-9][0-9] // year 116 | ( 117 | '-'[0-9][0-9] // month 118 | ( 119 | '-'[0-9][0-9] // day 120 | ( 121 | 'T' TIMEFORMAT 122 | )? 123 | )? 124 | )? 125 | 'Z'? // UTC specifier 126 | ; 127 | 128 | TIME 129 | : '@' 'T' TIMEFORMAT 130 | ; 131 | 132 | fragment TIMEFORMAT 133 | : 134 | [0-9][0-9] (':'[0-9][0-9] (':'[0-9][0-9] ('.'[0-9]+)?)?)? 135 | ('Z' | ('+' | '-') [0-9][0-9]':'[0-9][0-9])? // timezone 136 | ; 137 | 138 | IDENTIFIER 139 | : ([A-Za-z] | '_')([A-Za-z0-9] | '_')* // Added _ to support CQL (FHIR could constrain it out) 140 | ; 141 | 142 | DELIMITEDIDENTIFIER 143 | : '`' (ESC | ~[\\`])* '`' 144 | ; 145 | 146 | STRING 147 | : '\'' (ESC | ~['])* '\'' 148 | ; 149 | 150 | // Also allows leading zeroes now (just like CQL and XSD) 151 | NUMBER 152 | : [0-9]+('.' [0-9]+)? 153 | ; 154 | 155 | // Pipe whitespace to the HIDDEN channel to support retrieving source text through the parser. 156 | WS 157 | : [ \r\n\t]+ -> channel(HIDDEN) 158 | ; 159 | 160 | COMMENT 161 | : '/*' .*? '*/' -> channel(HIDDEN) 162 | ; 163 | 164 | LINE_COMMENT 165 | : '//' ~[\r\n]* -> channel(HIDDEN) 166 | ; 167 | 168 | fragment ESC 169 | : '\\' ([`'\\/fnrt] | UNICODE) // allow \`, \', \\, \/, \f, etc. and \uXXX 170 | ; 171 | 172 | fragment UNICODE 173 | : 'u' HEX HEX HEX HEX 174 | ; 175 | 176 | fragment HEX 177 | : [0-9a-fA-F] 178 | ; 179 | 180 | 181 | -------------------------------------------------------------------------------- /fhirpathpy/parser/README.md: -------------------------------------------------------------------------------- 1 | ## How To generate python code 2 | https://github.com/antlr/antlr4/blob/master/doc/python-target.md 3 | -------------------------------------------------------------------------------- /fhirpathpy/parser/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from antlr4 import * 3 | from antlr4.tree.Tree import ParseTreeWalker 4 | from antlr4.error.ErrorListener import ErrorListener 5 | from antlr4.error.Errors import LexerNoViableAltException 6 | from fhirpathpy.parser.generated.FHIRPathLexer import FHIRPathLexer 7 | from fhirpathpy.parser.generated.FHIRPathParser import FHIRPathParser 8 | from fhirpathpy.parser.ASTPathListener import ASTPathListener 9 | 10 | 11 | def recover(e): 12 | raise e 13 | 14 | 15 | def parse(value): 16 | textStream = InputStream(value) 17 | 18 | astPathListener = ASTPathListener() 19 | errorListener = ErrorListener() 20 | 21 | lexer = FHIRPathLexer(textStream) 22 | lexer.recover = recover 23 | lexer.removeErrorListeners() 24 | lexer.addErrorListener(errorListener) 25 | 26 | parser = FHIRPathParser(CommonTokenStream(lexer)) 27 | parser.buildParseTrees = True 28 | parser.removeErrorListeners() 29 | parser.addErrorListener(errorListener) 30 | 31 | walker = ParseTreeWalker() 32 | walker.walk(astPathListener, parser.expression()) 33 | 34 | return astPathListener.parentStack[0] 35 | -------------------------------------------------------------------------------- /fhirpathpy/parser/generated/FHIRPath.interp: -------------------------------------------------------------------------------- 1 | token literal names: 2 | null 3 | '.' 4 | '[' 5 | ']' 6 | '+' 7 | '-' 8 | '*' 9 | '/' 10 | 'div' 11 | 'mod' 12 | '&' 13 | '|' 14 | '<=' 15 | '<' 16 | '>' 17 | '>=' 18 | 'is' 19 | 'as' 20 | '=' 21 | '~' 22 | '!=' 23 | '!~' 24 | 'in' 25 | 'contains' 26 | 'and' 27 | 'or' 28 | 'xor' 29 | 'implies' 30 | '(' 31 | ')' 32 | '{' 33 | '}' 34 | 'true' 35 | 'false' 36 | '%' 37 | '$this' 38 | '$index' 39 | '$total' 40 | ',' 41 | 'year' 42 | 'month' 43 | 'week' 44 | 'day' 45 | 'hour' 46 | 'minute' 47 | 'second' 48 | 'millisecond' 49 | 'years' 50 | 'months' 51 | 'weeks' 52 | 'days' 53 | 'hours' 54 | 'minutes' 55 | 'seconds' 56 | 'milliseconds' 57 | null 58 | null 59 | null 60 | null 61 | null 62 | null 63 | null 64 | null 65 | null 66 | 67 | token symbolic names: 68 | null 69 | null 70 | null 71 | null 72 | null 73 | null 74 | null 75 | null 76 | null 77 | null 78 | null 79 | null 80 | null 81 | null 82 | null 83 | null 84 | null 85 | null 86 | null 87 | null 88 | null 89 | null 90 | null 91 | null 92 | null 93 | null 94 | null 95 | null 96 | null 97 | null 98 | null 99 | null 100 | null 101 | null 102 | null 103 | null 104 | null 105 | null 106 | null 107 | null 108 | null 109 | null 110 | null 111 | null 112 | null 113 | null 114 | null 115 | null 116 | null 117 | null 118 | null 119 | null 120 | null 121 | null 122 | null 123 | DATETIME 124 | TIME 125 | IDENTIFIER 126 | DELIMITEDIDENTIFIER 127 | STRING 128 | NUMBER 129 | WS 130 | COMMENT 131 | LINE_COMMENT 132 | 133 | rule names: 134 | entireExpression 135 | expression 136 | term 137 | literal 138 | externalConstant 139 | invocation 140 | functn 141 | paramList 142 | quantity 143 | unit 144 | dateTimePrecision 145 | pluralDateTimePrecision 146 | typeSpecifier 147 | qualifiedIdentifier 148 | identifier 149 | 150 | 151 | atn: 152 | [4, 1, 63, 154, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 38, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 78, 8, 1, 10, 1, 12, 1, 81, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 90, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 100, 8, 3, 1, 4, 1, 4, 1, 4, 3, 4, 105, 8, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 112, 8, 5, 1, 6, 1, 6, 1, 6, 3, 6, 117, 8, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 5, 7, 124, 8, 7, 10, 7, 12, 7, 127, 9, 7, 1, 8, 1, 8, 3, 8, 131, 8, 8, 1, 9, 1, 9, 1, 9, 3, 9, 136, 8, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 5, 13, 147, 8, 13, 10, 13, 12, 13, 150, 9, 13, 1, 14, 1, 14, 1, 14, 0, 1, 2, 15, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 0, 12, 1, 0, 4, 5, 1, 0, 6, 9, 2, 0, 4, 5, 10, 10, 1, 0, 12, 15, 1, 0, 18, 21, 1, 0, 22, 23, 1, 0, 25, 26, 1, 0, 16, 17, 1, 0, 32, 33, 1, 0, 39, 46, 1, 0, 47, 54, 3, 0, 16, 17, 22, 23, 57, 58, 171, 0, 30, 1, 0, 0, 0, 2, 37, 1, 0, 0, 0, 4, 89, 1, 0, 0, 0, 6, 99, 1, 0, 0, 0, 8, 101, 1, 0, 0, 0, 10, 111, 1, 0, 0, 0, 12, 113, 1, 0, 0, 0, 14, 120, 1, 0, 0, 0, 16, 128, 1, 0, 0, 0, 18, 135, 1, 0, 0, 0, 20, 137, 1, 0, 0, 0, 22, 139, 1, 0, 0, 0, 24, 141, 1, 0, 0, 0, 26, 143, 1, 0, 0, 0, 28, 151, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 34, 6, 1, -1, 0, 34, 38, 3, 4, 2, 0, 35, 36, 7, 0, 0, 0, 36, 38, 3, 2, 1, 11, 37, 33, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 38, 79, 1, 0, 0, 0, 39, 40, 10, 10, 0, 0, 40, 41, 7, 1, 0, 0, 41, 78, 3, 2, 1, 11, 42, 43, 10, 9, 0, 0, 43, 44, 7, 2, 0, 0, 44, 78, 3, 2, 1, 10, 45, 46, 10, 8, 0, 0, 46, 47, 5, 11, 0, 0, 47, 78, 3, 2, 1, 9, 48, 49, 10, 7, 0, 0, 49, 50, 7, 3, 0, 0, 50, 78, 3, 2, 1, 8, 51, 52, 10, 5, 0, 0, 52, 53, 7, 4, 0, 0, 53, 78, 3, 2, 1, 6, 54, 55, 10, 4, 0, 0, 55, 56, 7, 5, 0, 0, 56, 78, 3, 2, 1, 5, 57, 58, 10, 3, 0, 0, 58, 59, 5, 24, 0, 0, 59, 78, 3, 2, 1, 4, 60, 61, 10, 2, 0, 0, 61, 62, 7, 6, 0, 0, 62, 78, 3, 2, 1, 3, 63, 64, 10, 1, 0, 0, 64, 65, 5, 27, 0, 0, 65, 78, 3, 2, 1, 2, 66, 67, 10, 13, 0, 0, 67, 68, 5, 1, 0, 0, 68, 78, 3, 10, 5, 0, 69, 70, 10, 12, 0, 0, 70, 71, 5, 2, 0, 0, 71, 72, 3, 2, 1, 0, 72, 73, 5, 3, 0, 0, 73, 78, 1, 0, 0, 0, 74, 75, 10, 6, 0, 0, 75, 76, 7, 7, 0, 0, 76, 78, 3, 24, 12, 0, 77, 39, 1, 0, 0, 0, 77, 42, 1, 0, 0, 0, 77, 45, 1, 0, 0, 0, 77, 48, 1, 0, 0, 0, 77, 51, 1, 0, 0, 0, 77, 54, 1, 0, 0, 0, 77, 57, 1, 0, 0, 0, 77, 60, 1, 0, 0, 0, 77, 63, 1, 0, 0, 0, 77, 66, 1, 0, 0, 0, 77, 69, 1, 0, 0, 0, 77, 74, 1, 0, 0, 0, 78, 81, 1, 0, 0, 0, 79, 77, 1, 0, 0, 0, 79, 80, 1, 0, 0, 0, 80, 3, 1, 0, 0, 0, 81, 79, 1, 0, 0, 0, 82, 90, 3, 10, 5, 0, 83, 90, 3, 6, 3, 0, 84, 90, 3, 8, 4, 0, 85, 86, 5, 28, 0, 0, 86, 87, 3, 2, 1, 0, 87, 88, 5, 29, 0, 0, 88, 90, 1, 0, 0, 0, 89, 82, 1, 0, 0, 0, 89, 83, 1, 0, 0, 0, 89, 84, 1, 0, 0, 0, 89, 85, 1, 0, 0, 0, 90, 5, 1, 0, 0, 0, 91, 92, 5, 30, 0, 0, 92, 100, 5, 31, 0, 0, 93, 100, 7, 8, 0, 0, 94, 100, 5, 59, 0, 0, 95, 100, 5, 60, 0, 0, 96, 100, 5, 55, 0, 0, 97, 100, 5, 56, 0, 0, 98, 100, 3, 16, 8, 0, 99, 91, 1, 0, 0, 0, 99, 93, 1, 0, 0, 0, 99, 94, 1, 0, 0, 0, 99, 95, 1, 0, 0, 0, 99, 96, 1, 0, 0, 0, 99, 97, 1, 0, 0, 0, 99, 98, 1, 0, 0, 0, 100, 7, 1, 0, 0, 0, 101, 104, 5, 34, 0, 0, 102, 105, 3, 28, 14, 0, 103, 105, 5, 59, 0, 0, 104, 102, 1, 0, 0, 0, 104, 103, 1, 0, 0, 0, 105, 9, 1, 0, 0, 0, 106, 112, 3, 28, 14, 0, 107, 112, 3, 12, 6, 0, 108, 112, 5, 35, 0, 0, 109, 112, 5, 36, 0, 0, 110, 112, 5, 37, 0, 0, 111, 106, 1, 0, 0, 0, 111, 107, 1, 0, 0, 0, 111, 108, 1, 0, 0, 0, 111, 109, 1, 0, 0, 0, 111, 110, 1, 0, 0, 0, 112, 11, 1, 0, 0, 0, 113, 114, 3, 28, 14, 0, 114, 116, 5, 28, 0, 0, 115, 117, 3, 14, 7, 0, 116, 115, 1, 0, 0, 0, 116, 117, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 119, 5, 29, 0, 0, 119, 13, 1, 0, 0, 0, 120, 125, 3, 2, 1, 0, 121, 122, 5, 38, 0, 0, 122, 124, 3, 2, 1, 0, 123, 121, 1, 0, 0, 0, 124, 127, 1, 0, 0, 0, 125, 123, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 15, 1, 0, 0, 0, 127, 125, 1, 0, 0, 0, 128, 130, 5, 60, 0, 0, 129, 131, 3, 18, 9, 0, 130, 129, 1, 0, 0, 0, 130, 131, 1, 0, 0, 0, 131, 17, 1, 0, 0, 0, 132, 136, 3, 20, 10, 0, 133, 136, 3, 22, 11, 0, 134, 136, 5, 59, 0, 0, 135, 132, 1, 0, 0, 0, 135, 133, 1, 0, 0, 0, 135, 134, 1, 0, 0, 0, 136, 19, 1, 0, 0, 0, 137, 138, 7, 9, 0, 0, 138, 21, 1, 0, 0, 0, 139, 140, 7, 10, 0, 0, 140, 23, 1, 0, 0, 0, 141, 142, 3, 26, 13, 0, 142, 25, 1, 0, 0, 0, 143, 148, 3, 28, 14, 0, 144, 145, 5, 1, 0, 0, 145, 147, 3, 28, 14, 0, 146, 144, 1, 0, 0, 0, 147, 150, 1, 0, 0, 0, 148, 146, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 27, 1, 0, 0, 0, 150, 148, 1, 0, 0, 0, 151, 152, 7, 11, 0, 0, 152, 29, 1, 0, 0, 0, 12, 37, 77, 79, 89, 99, 104, 111, 116, 125, 130, 135, 148] -------------------------------------------------------------------------------- /fhirpathpy/parser/generated/FHIRPath.tokens: -------------------------------------------------------------------------------- 1 | T__0=1 2 | T__1=2 3 | T__2=3 4 | T__3=4 5 | T__4=5 6 | T__5=6 7 | T__6=7 8 | T__7=8 9 | T__8=9 10 | T__9=10 11 | T__10=11 12 | T__11=12 13 | T__12=13 14 | T__13=14 15 | T__14=15 16 | T__15=16 17 | T__16=17 18 | T__17=18 19 | T__18=19 20 | T__19=20 21 | T__20=21 22 | T__21=22 23 | T__22=23 24 | T__23=24 25 | T__24=25 26 | T__25=26 27 | T__26=27 28 | T__27=28 29 | T__28=29 30 | T__29=30 31 | T__30=31 32 | T__31=32 33 | T__32=33 34 | T__33=34 35 | T__34=35 36 | T__35=36 37 | T__36=37 38 | T__37=38 39 | T__38=39 40 | T__39=40 41 | T__40=41 42 | T__41=42 43 | T__42=43 44 | T__43=44 45 | T__44=45 46 | T__45=46 47 | T__46=47 48 | T__47=48 49 | T__48=49 50 | T__49=50 51 | T__50=51 52 | T__51=52 53 | T__52=53 54 | T__53=54 55 | DATETIME=55 56 | TIME=56 57 | IDENTIFIER=57 58 | DELIMITEDIDENTIFIER=58 59 | STRING=59 60 | NUMBER=60 61 | WS=61 62 | COMMENT=62 63 | LINE_COMMENT=63 64 | '.'=1 65 | '['=2 66 | ']'=3 67 | '+'=4 68 | '-'=5 69 | '*'=6 70 | '/'=7 71 | 'div'=8 72 | 'mod'=9 73 | '&'=10 74 | '|'=11 75 | '<='=12 76 | '<'=13 77 | '>'=14 78 | '>='=15 79 | 'is'=16 80 | 'as'=17 81 | '='=18 82 | '~'=19 83 | '!='=20 84 | '!~'=21 85 | 'in'=22 86 | 'contains'=23 87 | 'and'=24 88 | 'or'=25 89 | 'xor'=26 90 | 'implies'=27 91 | '('=28 92 | ')'=29 93 | '{'=30 94 | '}'=31 95 | 'true'=32 96 | 'false'=33 97 | '%'=34 98 | '$this'=35 99 | '$index'=36 100 | '$total'=37 101 | ','=38 102 | 'year'=39 103 | 'month'=40 104 | 'week'=41 105 | 'day'=42 106 | 'hour'=43 107 | 'minute'=44 108 | 'second'=45 109 | 'millisecond'=46 110 | 'years'=47 111 | 'months'=48 112 | 'weeks'=49 113 | 'days'=50 114 | 'hours'=51 115 | 'minutes'=52 116 | 'seconds'=53 117 | 'milliseconds'=54 118 | -------------------------------------------------------------------------------- /fhirpathpy/parser/generated/FHIRPathLexer.tokens: -------------------------------------------------------------------------------- 1 | T__0=1 2 | T__1=2 3 | T__2=3 4 | T__3=4 5 | T__4=5 6 | T__5=6 7 | T__6=7 8 | T__7=8 9 | T__8=9 10 | T__9=10 11 | T__10=11 12 | T__11=12 13 | T__12=13 14 | T__13=14 15 | T__14=15 16 | T__15=16 17 | T__16=17 18 | T__17=18 19 | T__18=19 20 | T__19=20 21 | T__20=21 22 | T__21=22 23 | T__22=23 24 | T__23=24 25 | T__24=25 26 | T__25=26 27 | T__26=27 28 | T__27=28 29 | T__28=29 30 | T__29=30 31 | T__30=31 32 | T__31=32 33 | T__32=33 34 | T__33=34 35 | T__34=35 36 | T__35=36 37 | T__36=37 38 | T__37=38 39 | T__38=39 40 | T__39=40 41 | T__40=41 42 | T__41=42 43 | T__42=43 44 | T__43=44 45 | T__44=45 46 | T__45=46 47 | T__46=47 48 | T__47=48 49 | T__48=49 50 | T__49=50 51 | T__50=51 52 | T__51=52 53 | T__52=53 54 | T__53=54 55 | DATETIME=55 56 | TIME=56 57 | IDENTIFIER=57 58 | DELIMITEDIDENTIFIER=58 59 | STRING=59 60 | NUMBER=60 61 | WS=61 62 | COMMENT=62 63 | LINE_COMMENT=63 64 | '.'=1 65 | '['=2 66 | ']'=3 67 | '+'=4 68 | '-'=5 69 | '*'=6 70 | '/'=7 71 | 'div'=8 72 | 'mod'=9 73 | '&'=10 74 | '|'=11 75 | '<='=12 76 | '<'=13 77 | '>'=14 78 | '>='=15 79 | 'is'=16 80 | 'as'=17 81 | '='=18 82 | '~'=19 83 | '!='=20 84 | '!~'=21 85 | 'in'=22 86 | 'contains'=23 87 | 'and'=24 88 | 'or'=25 89 | 'xor'=26 90 | 'implies'=27 91 | '('=28 92 | ')'=29 93 | '{'=30 94 | '}'=31 95 | 'true'=32 96 | 'false'=33 97 | '%'=34 98 | '$this'=35 99 | '$index'=36 100 | '$total'=37 101 | ','=38 102 | 'year'=39 103 | 'month'=40 104 | 'week'=41 105 | 'day'=42 106 | 'hour'=43 107 | 'minute'=44 108 | 'second'=45 109 | 'millisecond'=46 110 | 'years'=47 111 | 'months'=48 112 | 'weeks'=49 113 | 'days'=50 114 | 'hours'=51 115 | 'minutes'=52 116 | 'seconds'=53 117 | 'milliseconds'=54 118 | -------------------------------------------------------------------------------- /fhirpathpy/parser/generated/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beda-software/fhirpath-py/60d668d72b37c71b0bd771bfb5567143e9adba52/fhirpathpy/parser/generated/__init__.py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py311'] 4 | exclude = ''' 5 | ( 6 | /( 7 | | \.git 8 | | \.pytest_cache 9 | | pyproject.toml 10 | | dist 11 | )/ 12 | ) 13 | ''' 14 | 15 | [tool.pytest.ini_options] 16 | minversion = "6.0" 17 | addopts = "-ra -q --color=yes --cov=fhirpathpy --cov-report=xml" 18 | testpaths = ["tests"] 19 | log_cli = true 20 | log_cli_level = "INFO" 21 | python_functions = "*_test" 22 | 23 | [build-system] 24 | requires = ["flit_core >=3.2,<4"] 25 | build-backend = "flit_core.buildapi" 26 | 27 | [project] 28 | name = "fhirpathpy" 29 | description = "FHIRPath implementation in Python" 30 | readme = "README.md" 31 | license = { file = "LICENSE.md" } 32 | keywords = ["fhir", "fhirpath"] 33 | dynamic = ["version"] 34 | authors = [{ name = "beda.software", email = "fhirpath@beda.software" }] 35 | dependencies = ["antlr4-python3-runtime~=4.10", "python-dateutil~=2.8"] 36 | classifiers = [ 37 | "Development Status :: 5 - Production/Stable", 38 | "Environment :: Web Environment", 39 | "Intended Audience :: Developers", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.8", 44 | "Programming Language :: Python :: 3.9", 45 | "Programming Language :: Python :: 3.10", 46 | "Programming Language :: Python :: 3.11", 47 | "Topic :: Internet :: WWW/HTTP", 48 | "Topic :: Software Development :: Libraries :: Python Modules", 49 | ] 50 | requires-python = ">=3.8" 51 | 52 | [project.optional-dependencies] 53 | test = ["pytest==7.1.1", "pyyaml==5.4"] 54 | 55 | [project.urls] 56 | Homepage = "https://github.com/beda-software/fhirpath-py" 57 | Documentation = "https://github.com/beda-software/fhirpath-py#readme" 58 | Source = "https://github.com/beda-software/fhirpath-py.git" 59 | Changelog = "https://github.com/beda-software/fhirpath-py/blob/master/CHANGELOG.md" 60 | 61 | 62 | [tool.ruff] 63 | target-version = "py39" 64 | line-length = 100 65 | include = ["app/**/*.py", "tests/**/*.py"] 66 | 67 | [tool.ruff.lint] 68 | select = ["B", "F", "I", "E", "UP", "N", "PL", "PERF"] 69 | # Black is responsible for E501 70 | ignore = ["E501"] 71 | unfixable = ["F401"] 72 | 73 | [tool.autohooks] 74 | mode = "pipenv" 75 | pre-commit = ["autohooks.plugins.black", "autohooks.plugins.ruff"] 76 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beda-software/fhirpath-py/60d668d72b37c71b0bd771bfb5567143e9adba52/tests/__init__.py -------------------------------------------------------------------------------- /tests/cases/3.2_paths.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - 'group: Paths with choice types': 3 | - desc: 'Observation.value with an R5 FHIR model' 4 | expression: 'Observation.value' 5 | result: ["high"] 6 | model: 'r5' 7 | 8 | - desc: 'Observation.value with an R4 FHIR model' 9 | expression: 'Observation.value' 10 | result: ["high"] 11 | model: 'r4' 12 | 13 | - desc: 'Observation.value with an STU3 FHIR model' 14 | expression: 'Observation.value' 15 | result: ["high"] 16 | model: 'stu3' 17 | 18 | - desc: 'Observation.value with an DSTU2 FHIR model' 19 | expression: 'Observation.value' 20 | result: ["high"] 21 | model: 'dstu2' 22 | 23 | - desc: 'Observation.value without a model' 24 | expression: 'Observation.value' 25 | result: [] 26 | 27 | - desc: 'Observation.value contained in another resource (1)' 28 | expression: 'Observation.contained[0].value' 29 | model: 'r4' 30 | result: ['medium'] 31 | 32 | - desc: 'Observation.value contained in another resource (2)' 33 | expression: 'Observation.contained.value' 34 | model: 'r4' 35 | result: ['medium', 'low'] 36 | 37 | - desc: 'Getting choice type fields via children()' 38 | expression: 'Observation.children().value' 39 | model: 'r4' 40 | result: ['medium', 'low'] 41 | 42 | - desc: 'Getting choice type fields via children() (2)' 43 | expression: 'Observation.children().children().value' 44 | model: 'r4' 45 | result: ['zero'] 46 | 47 | - desc: 'Getting choice type fields via descendants()' 48 | expression: 'Observation.descendants().value' 49 | model: 'r4' 50 | result: ['medium', 'low', 'zero', 'Red', 'Blue', 'Green'] 51 | 52 | - desc: 'Getting choice type fields via descendants() and where()' 53 | expression: "Observation.descendants().where(resourceType = 'Observation').value" 54 | model: 'r4' 55 | result: ['medium', 'low', 'zero'] 56 | 57 | - desc: "QR with where()" 58 | expression: "contained.where(resourceType = 'QuestionnaireResponse').item.where(linkId = '1').answer.value" 59 | model: 'r4' 60 | result: ['Red'] 61 | 62 | - desc: "QR with descendants() and where() (1)" 63 | expression: "contained.where(resourceType = 'QuestionnaireResponse').descendants().where(linkId = '1').answer.value" 64 | model: 'r4' 65 | result: ['Red'] 66 | 67 | - desc: "QR with descendants() and where() (2)" 68 | expression: "contained.where(resourceType = 'QuestionnaireResponse').descendants().where(linkId = '1.0').answer.value" 69 | model: 'r4' 70 | result: ['Green'] 71 | 72 | - desc: "QR with descendants() and where() (3)" 73 | expression: "contained.where(resourceType = 'QuestionnaireResponse').descendants().where(linkId = '1.1').answer.value" 74 | model: 'r4' 75 | result: ['Blue'] 76 | 77 | - desc: "QR with item.answer.value" 78 | expression: "contained.where(resourceType = 'QuestionnaireResponse').item.answer.value" 79 | model: 'r4' 80 | result: ['Red'] 81 | 82 | - desc: "QR with item.item.answer.value" 83 | expression: "contained.where(resourceType = 'QuestionnaireResponse').item.item.answer.value" 84 | model: 'r4' 85 | result: ['Blue'] 86 | 87 | - desc: "QR with item.answer.item.answer.value" 88 | expression: "contained.where(resourceType = 'QuestionnaireResponse').item.answer.item.answer.value" 89 | model: 'r4' 90 | result: ['Green'] 91 | 92 | - desc: "QR with item.answer.item.answer.value (STU3)" 93 | expression: "contained.where(resourceType = 'QuestionnaireResponse').item.answer.item.answer.value" 94 | model: 'stu3' 95 | result: ['Green'] 96 | 97 | 98 | subject: 99 | resourceType: Observation 100 | valueString: "high" 101 | contained: 102 | - resourceType: Observation 103 | valueString: "medium" 104 | - resourceType: Observation 105 | valueString: "low" 106 | contained: 107 | - resourceType: Observation 108 | valueString: "zero" 109 | - resourceType: QuestionnaireResponse 110 | item: 111 | - linkId: "1" 112 | answer: 113 | - valueString: "Red" 114 | item: 115 | - linkId: "1.0" 116 | answer: 117 | - valueString: "Green" 118 | item: 119 | - linkId: "1.1" 120 | answer: 121 | - valueString: "Blue" 122 | -------------------------------------------------------------------------------- /tests/cases/4.1_literals.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - expression: '{}' 3 | result: [] 4 | - expression: "'a\\\\b\\'\\\"\\`\\r\\n\\t\\u0065'" # double escape for YAML 5 | result: ["a\\b'\"`\r\n\te"] 6 | - expression: "\"a\\\\b\\'\\\"\\`\\r\\n\\t\\u0065\"" # using " to wrap a string is not allowed by grammar 7 | error: true 8 | - expression: "`a\\\\b\\'\\\"\\`\\r\\n\\t\\u0065`" # using ` to wrap a string is not allowed by grammar 9 | error: true 10 | - expression: "2 'mo'" 11 | result: ["2 'mo'"] 12 | - expression: "2 years" 13 | result: ["2 years"] 14 | - expression: "(2 years).value" # test of internal structure, not FHIRPath 15 | result: [2] 16 | - expression: "@2019" 17 | result: ["2019"] 18 | - expression: "@2019-02-03T01:00Z = @2019-02-02T21:00-04:00" 19 | result: [true] 20 | - expression: "@2019-02-03T02:00Z = @2019-02-02T21:00-04:00" 21 | result: [false] 22 | - expression: '-7' 23 | result: [-7] 24 | - expression: '-7.3' 25 | result: [-7.3] 26 | - expression: '+7' 27 | result: [+7] 28 | - expression: '(-7).combine(3)' 29 | result: [-7, 3] 30 | - expression: '-7.combine(3)' # same as next case 31 | error: true 32 | - expression: '-((7).combine(3))' 33 | error: true # per Bryn 34 | - expression: '-true' 35 | error: true 36 | - expression: "-'zzz'" 37 | error: true 38 | 39 | subject: 40 | prop: val 41 | -------------------------------------------------------------------------------- /tests/cases/5.2.3_repeat.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - expression: Questionnaire.repeat(item).linkId 3 | result: ["1", "2", "1.1", "2.1", "1.1.1", "2.1.2", "1.1.1.1", "1.1.1.2", "1.1.1.1.1", "1.1.1.1.2"] 4 | - expression: Questionnaire.combine(Questionnaire).repeat(item).linkId 5 | result: ["1", "2", "1.1", "2.1", "1.1.1", "2.1.2", "1.1.1.1", "1.1.1.2", "1.1.1.1.1", "1.1.1.1.2"] 6 | subject: 7 | resourceType: Questionnaire 8 | id: '3141' 9 | url: http://hl7.org/fhir/Questionnaire/3141 10 | title: Cancer Quality Forum Questionnaire 2012 11 | status: draft 12 | date: 2012-01 13 | subjectType: 14 | - Patient 15 | item: 16 | - linkId: '1' 17 | code: 18 | - system: http://example.org/system/code/sections 19 | code: COMORBIDITY 20 | type: group 21 | item: 22 | - linkId: '1.1' 23 | code: 24 | - system: http://example.org/system/code/questions 25 | code: COMORB 26 | prefix: '1' 27 | type: choice 28 | options: 29 | reference: http://hl7.org/fhir/ValueSet/yesnodontknow 30 | item: 31 | - linkId: 1.1.1 32 | code: 33 | - system: http://example.org/system/code/sections 34 | code: CARDIAL 35 | type: group 36 | enableWhen: 37 | - question: '1.1' 38 | answerCoding: 39 | system: http://hl7.org/fhir/v2/0136 40 | code: Y 41 | item: 42 | - linkId: 1.1.1.1 43 | code: 44 | - system: http://example.org/system/code/questions 45 | code: COMORBCAR 46 | prefix: '1.1' 47 | type: choice 48 | options: 49 | reference: http://hl7.org/fhir/ValueSet/yesnodontknow 50 | item: 51 | - linkId: 1.1.1.1.1 52 | code: 53 | - system: http://example.org/system/code/questions 54 | code: COMCAR00 55 | display: Angina Pectoris 56 | - system: http://snomed.info/sct 57 | code: '194828000' 58 | display: Angina (disorder) 59 | prefix: 1.1.1 60 | type: choice 61 | options: 62 | reference: http://hl7.org/fhir/ValueSet/yesnodontknow 63 | - linkId: 1.1.1.1.2 64 | code: 65 | - system: http://snomed.info/sct 66 | code: '22298006' 67 | display: Myocardial infarction (disorder) 68 | prefix: 1.1.2 69 | type: choice 70 | options: 71 | reference: http://hl7.org/fhir/ValueSet/yesnodontknow 72 | - linkId: 1.1.1.2 73 | code: 74 | - system: http://example.org/system/code/questions 75 | code: COMORBVAS 76 | prefix: '1.2' 77 | type: choice 78 | options: 79 | reference: http://hl7.org/fhir/ValueSet/yesnodontknow 80 | - linkId: '2' 81 | code: 82 | - system: http://example.org/system/code/sections 83 | code: HISTOPATHOLOGY 84 | type: group 85 | item: 86 | - linkId: '2.1' 87 | code: 88 | - system: http://example.org/system/code/sections 89 | code: ABDOMINAL 90 | type: group 91 | item: 92 | - linkId: 2.1.2 93 | code: 94 | - system: http://example.org/system/code/questions 95 | code: STADPT 96 | display: pT category 97 | type: choice 98 | -------------------------------------------------------------------------------- /tests/cases/5.2_filtering_and_projection.yaml: -------------------------------------------------------------------------------- 1 | 2 | tests: 3 | - desc: '5. Functions' 4 | - desc: '5.2. Filtering and projection' 5 | - desc: '5.2.1. where(criteria : expression) : collection' 6 | # Returns a collection containing only those elements in the input collection for which the stated criteria expression evaluates to true. Elements for which the expression evaluates to false or empty ({ }) are not included in the result. 7 | 8 | - desc: '** filter coll of numbers' 9 | expression: Functions.coll1.coll2.attr.where($this > 2) 10 | result: [3, 4, 5] 11 | 12 | - desc: '** filter coll with empty coll result' 13 | expression: Functions.coll1.coll2.attr.where($this = 0) 14 | result: [] 15 | 16 | - desc: '** the ability to use $index in the expression' 17 | expression: Functions.coll1.coll2.attr.where($index > 2) 18 | result: [4, 5] 19 | 20 | # If the input collection is emtpy ({ }), the result is empty. 21 | 22 | - desc: '** filter empty coll' 23 | expression: Functions.attrempty.where($this > 0) 24 | result: [] 25 | 26 | - desc: '** filter non-exists coll' 27 | expression: Functions.nothing.where($this < 0) 28 | result: [] 29 | 30 | - desc: '5.2.2. select(projection: expression) : collection' 31 | # Evaluates the projection expression for each item in the input collection. The result of each evaluation is added to the output collection. If the evaluation results in a collection with multiple items, all items are added to the output collection (collections resulting from evaluation of projection are flattened). This means that if the evaluation for an element results in the empty collection ({ }), no element is added to the result, and that if the input collection is empty ({ }), the result is empty as well. 32 | 33 | - desc: '** simple select' 34 | expression: Functions.coll1.coll2.select(attr) 35 | result: [1, 2, 3, 4, 5] 36 | 37 | - desc: '** select 2' 38 | expression: Functions.coll1.select(colltrue | collfalse).attr 39 | result: [true, false] 40 | 41 | - desc: '** select 3' 42 | expression: Functions.coll1.select(colltrue.attr | collfalse.attr) 43 | result: [true, false] 44 | 45 | - desc: '** select on empty coll is empty' 46 | expression: Functions.attrempty.select(nothing) 47 | result: [] 48 | 49 | - desc: '** the ability to use $index in the expression' 50 | expression: Functions.coll1.coll2.select(attr + $index) 51 | result: [1, 3, 5, 7, 9] 52 | 53 | - desc: '5.2.3. repeat(projection: expression) : collection' 54 | # A version of select that will repeat the projection and add it to the output collection, as long as the projection yields new items (as determined by the equals (=) operator). 55 | 56 | # This operation can be used to traverse a tree and selecting only specific children: 57 | 58 | # ValueSet.expansion.repeat(contains) 59 | # Will repeat finding children called contains, until no new nodes are found. 60 | 61 | # Questionnaire.repeat(group | question).question 62 | # Will repeat finding children called group or question, until no new nodes are found. 63 | 64 | # Note that this is slightly different from: 65 | 66 | # Questionnaire.descendants().select(group | question) 67 | # which would find any descendants called group or question, not just the ones nested inside other group or question elements. 68 | - desc: '* traverse tree' 69 | - desc: '** should not result in an infinite loop 1' 70 | expression: Functions.coll1.colltrue.repeat(true) 71 | result: [true] 72 | - desc: '** should not result in an infinite loop 2' 73 | expression: (1 | 2).repeat('item') 74 | result: ['item'] 75 | - desc: '** should use year-to-month conversion factor (https://hl7.org/fhirpath/#equals)' 76 | expression: (1 year).combine(12 months).repeat($this) 77 | result: 78 | - 1 year 79 | - desc: '** should compare quantities for equality (https://hl7.org/fhirpath/#equals)' 80 | expression: (3 'min').combine(180 seconds).repeat($this) 81 | result: 82 | - 3 'min' 83 | - desc: '** find all attrs' 84 | expression: Functions.repeat(repeatingAttr).count() 85 | result: [2] 86 | 87 | - desc: '** find all repeatingAttr.a values' 88 | expression: Functions.repeat(repeatingAttr).a 89 | result: [2, 1] 90 | 91 | - desc: '** find all true values in nested coll' 92 | expression: Functions.coll1.colltrue.repeat(attr) 93 | result: [true] 94 | 95 | - desc: '** find non-exists value' 96 | expression: Functions.coll1.repeat(nothing) 97 | result: [] 98 | 99 | 100 | - desc: '5.2.4. ofType(type : identifier) : collection' 101 | # Returns a collection that contains all items in the input collection that are of the given type or a subclass thereof. If the input collection is empty ({ }), the result is empty. 102 | - desc: '** empty input coll' 103 | expression: Functions.attrempty.ofType(string) 104 | result: [] 105 | - desc: '** string' 106 | expression: heteroattr.ofType(string) 107 | result: ['string'] 108 | - desc: '** integer' 109 | expression: heteroattr.ofType(integer) 110 | result: [1] 111 | - desc: '** decimal' 112 | expression: heteroattr.ofType(decimal) 113 | result: [1.01] 114 | - desc: '** boolean' 115 | expression: heteroattr.ofType(boolean) 116 | result: [true, false] 117 | - desc: '** object (not fhir)' 118 | expression: heteroattr.ofType(object) 119 | result: [{a: 1}] 120 | 121 | subject: 122 | resourceType: Functions 123 | attrempty: [] 124 | attrtrue: true 125 | attrfalse: false 126 | heteroattr: 127 | - 'string' 128 | - 1 129 | - 1.01 130 | - a: 1 131 | - true 132 | - false 133 | attrsingle: 134 | - 1 135 | attrdouble: 136 | - 1 137 | - 2 138 | attrobject: 139 | a: 1 140 | b: 2 141 | repeatingAttr: 5 142 | repeatingAttr: 143 | repeatingAttr: 144 | a: 1 145 | a: 2 146 | coll1: 147 | - coll2: 148 | - attr: 1 149 | - attr: 2 150 | - attr: 3 151 | - coll2: 152 | - attr: 4 153 | - attr: 5 154 | - colltrue: 155 | - attr: true 156 | - attr: true 157 | - attr: true 158 | - collwithfalse: 159 | - attr: false 160 | - attr: true 161 | - collfalse: 162 | - attr: false 163 | - attr: false 164 | - mixed: 165 | - attr: true 166 | - attr: false 167 | - attr: 'test string' 168 | - attr: 999 169 | - attr: 3.14159 170 | - attr: '@2015-02-04T14:34:28Z' 171 | - attr: '@T14:34:28+09:00' 172 | - attr: 4 days 173 | -------------------------------------------------------------------------------- /tests/cases/5.3_subsetting.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - desc: '5. Functions' 3 | - desc: '5.3. Subsetting 5.3.1.' 4 | - desc: '[ index : integer ] : collection' 5 | # The indexer operation 6 | # returns a collection with only the index-th item (0-based index). If the input 7 | # collection is empty ({ }), or the index lies outside the boundaries of the 8 | # input collection, an empty collection is returned. 9 | 10 | # Example: 11 | 12 | # Patient.name[0] 13 | 14 | - desc: '** [index]' 15 | expression: Functions.coll1[1].coll2[0].attr 16 | result: [4] 17 | 18 | - desc: '** [big index]' 19 | expression: Functions.coll1[100].coll2[0].attr 20 | result: [] 21 | 22 | - desc: '5.3.2. single() : collection' 23 | # Will return the single item in the input if there 24 | # is just one item. If the input collection is empty ({ }), the result is empty. 25 | # If there are multiple items, an error is signaled to the evaluation 26 | # environment. This operation is useful for ensuring that an error is returned 27 | # if an assumption about cardinality is violated at run-time. 28 | - desc: '** single' 29 | expression: Functions.attrsingle.single() 30 | result: [1] 31 | 32 | - desc: '** single on nothing' 33 | expression: Functions.ups.single() 34 | result: [] 35 | 36 | - desc: '** single on empty' 37 | expression: Functions.attrempty.single() 38 | result: [] 39 | 40 | - desc: '** single on many' 41 | expression: Functions.attrdouble.single() 42 | result: 43 | - $status: error 44 | $error: Expected single 45 | 46 | - desc: '5.3.3. first() : collection' 47 | # Returns a collection containing only the first 48 | # item in the input collection. This function is equivalent to item(0), so it 49 | # will return an empty collection if the input collection has no items. 50 | - desc: '** first' 51 | expression: Functions.attrdouble.first() 52 | result: [1] 53 | 54 | - desc: '** first nothing' 55 | expression: Functions.nothing.first() 56 | result: [] 57 | 58 | - desc: '5.3.4. last() : collection' 59 | # Returns a collection containing only the last item 60 | # in the input collection. Will return an empty collection if the input 61 | # collection has no items. 62 | 63 | - desc: '** last' 64 | expression: Functions.attrdouble.last() 65 | result: [2] 66 | 67 | - desc: '** last (alternative)' 68 | expression: Functions.attrsingle.last() 69 | result: [1] 70 | 71 | - desc: '** last on empty' 72 | expression: Functions.attrempty.last() 73 | result: [] 74 | 75 | - desc: '** last nothing' 76 | expression: Functions.nothing.last() 77 | result: [] 78 | 79 | - desc: '5.3.5. tail() : collection' 80 | # Returns a collection containing all but the first 81 | # item in the input collection. Will return an empty collection if the input 82 | # collection has no items, or only one item. 83 | 84 | - desc: '** tail' 85 | expression: Functions.attrdouble.tail() 86 | result: [ 2 ] 87 | 88 | - desc: '** tail on one' 89 | expression: Functions.attrsingle.tail() 90 | result: [ ] 91 | 92 | - desc: '** tail on empty' 93 | expression: Functions.attrempty.tail() 94 | result: [] 95 | 96 | - desc: '** tail nothing' 97 | expression: Functions.nothing.tail() 98 | result: [] 99 | 100 | - desc: '** tail (alternative)' 101 | expression: Functions.coll1.coll2.attr.tail() 102 | result: [2, 3, 4, 5] 103 | 104 | 105 | - desc: '5.3.6. skip(num : integer) : collection' 106 | # Returns a collection containing all 107 | # but the first num items in the input collection. Will return an empty 108 | # collection if there are no items remaining after the indicated number of items 109 | # have been skipped, or if the input collection is empty. If num is less than or 110 | # equal to zero, the input collection is simply returned. 111 | - desc: '** skip' 112 | expression: Functions.attrdouble.skip(1) 113 | result: [2] 114 | 115 | - desc: '** skip 2' 116 | expression: Functions.attrsingle.skip(1) 117 | result: [] 118 | 119 | - desc: '** skip 3' 120 | expression: Functions.coll1.coll2.attr.skip(3) 121 | result: [4, 5] 122 | 123 | - desc: '** skip 4' 124 | expression: Functions.coll1.coll2.attr.skip(4) 125 | result: [5] 126 | 127 | - desc: '** skip 5' 128 | expression: Functions.coll1.coll2.attr.skip(5) 129 | result: [] 130 | 131 | - desc: '** skip 6' 132 | expression: Functions.coll1.coll2.attr.skip(6) 133 | result: [] 134 | 135 | 136 | - desc: '5.3.7. take(num : integer) : collection' 137 | # Returns a collection containing the 138 | # first num items in the input collection, or less if there are less than num 139 | # items. If num is less than or equal to 0, or if the input collection is empty 140 | # ({ }), take returns an empty collection. 141 | 142 | - desc: '** take' 143 | expression: Functions.attrdouble.take(1) 144 | result: [ 1 ] 145 | 146 | - desc: '** take 2' 147 | expression: Functions.attrdouble.take(2) 148 | result: [ 1, 2 ] 149 | 150 | - desc: '** take more then has' 151 | expression: Functions.attrsingle.take(2) 152 | result: [ 1 ] 153 | 154 | - desc: '** take on empty' 155 | expression: Functions.attrempty.take(1) 156 | result: [] 157 | 158 | - desc: '** take nothing' 159 | expression: Functions.nothing.take(2) 160 | result: [] 161 | 162 | - desc: '** take 3' 163 | expression: Functions.coll1.coll2.attr.take(3) 164 | result: [1, 2, 3] 165 | 166 | - desc: '** take 4' 167 | expression: Functions.coll1.coll2.attr.take(4) 168 | result: [1, 2, 3, 4] 169 | 170 | - desc: '** take 5' 171 | expression: Functions.coll1.coll2.attr.take(5) 172 | result: [1, 2, 3, 4, 5] 173 | 174 | - desc: '5.3.8. intersect(other: collection) : collection' 175 | - desc: '** should not depend on the order of properties in an object' 176 | expression: Functions.objects.group1.intersect(Functions.objects.group2) 177 | result: 178 | - prop1: 1 179 | prop2: 2 180 | 181 | - desc: '** should use year-to-month conversion factor (https://hl7.org/fhirpath/#equals)' 182 | expression: (1 year).combine(12 months).intersect(12 months) 183 | result: 184 | - 1 year 185 | 186 | - desc: '** should compare quantities for equality (https://hl7.org/fhirpath/#equals)' 187 | expression: (3 'min').combine(180 seconds).intersect(180 seconds) 188 | result: 189 | - 3 'min' 190 | 191 | subject: 192 | resourceType: Functions 193 | attrempty: [] 194 | attrtrue: true 195 | attrfalse: false 196 | attrsingle: 197 | - 1 198 | attrdouble: 199 | - 1 200 | - 2 201 | attrobject: 202 | a: 1 203 | b: 2 204 | repeatingAttr: 5 205 | repeatingAttr: 206 | repeatingAttr: 207 | a: 1 208 | a: 2 209 | coll1: 210 | - coll2: 211 | - attr: 1 212 | - attr: 2 213 | - attr: 3 214 | - coll2: 215 | - attr: 4 216 | - attr: 5 217 | - colltrue: 218 | - attr: true 219 | - attr: true 220 | - attr: true 221 | - collwithfalse: 222 | - attr: false 223 | - attr: true 224 | - collfalse: 225 | - attr: false 226 | - attr: false 227 | - mixed: 228 | - attr: true 229 | - attr: false 230 | - attr: 'test string' 231 | - attr: 999 232 | - attr: 3.14159 233 | - attr: '@2015-02-04T14:34:28Z' 234 | - attr: '@T14:34:28+09:00' 235 | - attr: 4 days 236 | objects: 237 | group1: 238 | - prop1: 1 239 | prop2: 2 240 | - prop1: 1 241 | prop2: 2 242 | - prop1: 3 243 | prop2: 4 244 | group2: 245 | - prop2: 2 246 | prop1: 1 247 | - prop2: 2 248 | prop1: 1 249 | - prop1: 5 250 | prop2: 6 251 | -------------------------------------------------------------------------------- /tests/cases/5.4_combining.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - desc: '5. Functions' 3 | - desc: '5.4. Combining' 4 | - desc: '5.4.1. | (union collections)' 5 | # Merge the two collections into a single collection, eliminating any duplicate values (using equals (=)) to determine equality). Unioning an empty collection to a non-empty collection will return the non-empty collection with duplicates eliminated. There is no expectation of order in the resulting collection. 6 | 7 | - desc: '** Unioning empty coll with non-exists coll is empty coll' 8 | expression: Functions.coll1.nothing | Functions.attrempty 9 | result: [] 10 | 11 | - desc: '** Unioning empty coll with non-distinct coll is coll without duplicates' 12 | expression: Functions.attrempty | Functions.coll1.colltrue.attr 13 | result: [true] 14 | 15 | - desc: '** Unioning colls' 16 | expression: Functions.attrdouble | Functions.coll1.colltrue.attr 17 | result: [1, 2, true] 18 | 19 | - desc: '** Unioning colls 2' 20 | expression: Functions.attrdouble | Functions.coll1.coll2.attr 21 | result: [1, 2, 3, 4, 5] 22 | 23 | - desc: '** should use year-to-month conversion factor (https://hl7.org/fhirpath/#equals)' 24 | expression: (1 year | 12 months) 25 | result: 26 | - 1 year 27 | 28 | - desc: '** should compare quantities for equality (https://hl7.org/fhirpath/#equals)' 29 | expression: (3 'min' | 180 seconds) 30 | result: 31 | - 3 'min' 32 | 33 | - desc: '** should not depend on the order of properties in an object' 34 | expression: Functions.objects.group1 | Functions.objects.group2 35 | result: 36 | - prop1: 1 37 | prop2: 2 38 | - prop1: 3 39 | prop2: 4 40 | - prop1: 5 41 | prop2: 6 42 | 43 | - desc: '5.4.2. combine(other : collection) : collection' 44 | # Merge the input and other collections into a single collection without eliminating duplicate values. Combining an empty collection with a non-empty collection will return the non-empty collection. There is no expectation of order in the resulting collection. 45 | 46 | - desc: '** Combine empty coll with non-exists coll is empty coll' 47 | expression: Functions.attrempty.combine(Functions.nothing) 48 | result: [] 49 | 50 | - desc: '** Combine empty coll with non-empty coll' 51 | expression: Functions.attrempty.combine(Functions.coll1.colltrue.attr) 52 | result: [true, true, true] 53 | 54 | - desc: '** Combine colls' 55 | expression: Functions.attrdouble.combine(Functions.coll1.colltrue.attr) 56 | result: [1, 2, true, true, true] 57 | 58 | - desc: '** Combine colls 2' 59 | expression: Functions.attrdouble.combine(Functions.coll1.coll2.attr) 60 | result: [1, 2, 1, 2, 3, 4, 5] 61 | 62 | subject: 63 | resourceType: Functions 64 | attrempty: [] 65 | attrtrue: true 66 | attrfalse: false 67 | attrsingle: 68 | - 1 69 | attrdouble: 70 | - 1 71 | - 2 72 | attrobject: 73 | a: 1 74 | b: 2 75 | repeatingAttr: 5 76 | repeatingAttr: 77 | repeatingAttr: 78 | a: 1 79 | a: 2 80 | coll1: 81 | - coll2: 82 | - attr: 1 83 | - attr: 2 84 | - attr: 3 85 | - coll2: 86 | - attr: 4 87 | - attr: 5 88 | - colltrue: 89 | - attr: true 90 | - attr: true 91 | - attr: true 92 | - collwithfalse: 93 | - attr: false 94 | - attr: true 95 | - collfalse: 96 | - attr: false 97 | - attr: false 98 | - mixed: 99 | - attr: true 100 | - attr: false 101 | - attr: 'test string' 102 | - attr: 999 103 | - attr: 3.14159 104 | - attr: '@2015-02-04T14:34:28Z' 105 | - attr: '@T14:34:28+09:00' 106 | - attr: 4 days 107 | objects: 108 | group1: 109 | - prop1: 1 110 | prop2: 2 111 | - prop1: 3 112 | prop2: 4 113 | group2: 114 | - prop2: 2 115 | prop1: 1 116 | - prop1: 5 117 | prop2: 6 118 | -------------------------------------------------------------------------------- /tests/cases/5.7_math.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - desc: '5.7 Math' 3 | 4 | - desc: '5.7.1 abs() : Integer | Decimal | Quantity' 5 | - desc: '** Can take the absolute value of a number' 6 | expression: Math.d2.abs() 7 | result: [1.1] 8 | # - desc: '** Can return a number equal to the input if input value is a quantity' 9 | # expression: Math.quan.abs() 10 | # result: [4.5 'mg'] 11 | - desc: '** Empty result when taking the absolute value of empty collection' 12 | expression: Math.n2.abs() 13 | result: [] 14 | - desc: '** Error taking the absolute value due to too many input parameters' 15 | expression: Math.n3.abs(n4) 16 | error: true 17 | - desc: '** Error taking the absolute value if the input collection contains multiple items' 18 | expression: Math.arr.abs() 19 | error: true 20 | 21 | - desc: '5.7.2 ceiling() : Integer' 22 | - desc: '** Can return a number equal to the input' 23 | expression: Math.n1.ceiling() 24 | result: [1] 25 | - desc: '** Can round a number upward to its nearest integer' 26 | expression: Math.d1.ceiling() 27 | result: [2] 28 | - desc: '** Can round a number upward to its nearest integer' 29 | expression: Math.d2.ceiling() 30 | result: [-1] 31 | - desc: '** Empty result when rounding a number upward to its nearest integer from empty collection' 32 | expression: Math.n2.ceiling() 33 | result: [] 34 | - desc: '** Error rounding a number due to too many input parameters' 35 | expression: Math.n3.ceiling(n4) 36 | error: true 37 | - desc: '** Error rounding a number if the input collection contains multiple items' 38 | expression: Math.arr.ceiling() 39 | error: true 40 | 41 | - desc: '5.7.3 exp() : Decimal' 42 | - desc: '** Can raise e to the input number power' 43 | expression: Math.n0.exp() 44 | result: [1] 45 | - desc: '** Empty result for empty degree' 46 | expression: Math.n2.exp() 47 | result: [] 48 | - desc: '** Error exponentiation due to too many input parameters' 49 | expression: Math.n3.exp(n4) 50 | error: true 51 | - desc: '** Error exponentiation if the input collection contains multiple items' 52 | expression: Math.arr.exp() 53 | error: true 54 | 55 | - desc: '5.7.4 floor() : Integer' 56 | - desc: '** Can return a number equal to the input' 57 | expression: Math.n1.floor() 58 | result: [1] 59 | - desc: '** Can round a number downward to its nearest integer' 60 | expression: Math.d1.floor() 61 | result: [1] 62 | - desc: '** Can round a number downward to its nearest integer' 63 | expression: Math.d2.floor() 64 | result: [-2] 65 | - desc: '** Empty result when rounding a number downward to its nearest integer from empty collection' 66 | expression: Math.n2.floor() 67 | result: [] 68 | - desc: '** Error rounding a number due to too many input parameters' 69 | expression: Math.n3.floor(n4) 70 | error: true 71 | - desc: '** Error rounding a number if the input collection contains multiple items' 72 | expression: Math.arr.floor() 73 | error: true 74 | 75 | - desc: '5.7.5 ln() : Decimal' 76 | - desc: '** Can take the natural logarithm of the number' 77 | expression: Math.n1.ln() 78 | result: [0] 79 | - desc: '** Empty result when taking logarithm from empty collection' 80 | expression: Math.n2.ln() 81 | result: [] 82 | - desc: '** Error taking logarithm due to too many input parameters' 83 | expression: Math.n3.ln(n4) 84 | error: true 85 | - desc: '** Error taking logarithm if the input collection contains multiple items' 86 | expression: Math.arr.ln() 87 | error: true 88 | 89 | - desc: '5.7.6 log(base : Decimal) : Decimal' 90 | - desc: '** Can take the logarithm of the number with a given base' 91 | expression: Math.n4.log(2) 92 | result: [3] 93 | - desc: '** Empty result when taking logarithm from empty collection' 94 | expression: Math.n2.log(8) 95 | result: [] 96 | - desc: '** Empty result when taking logarithm with empty base' 97 | expression: Math.n3.log(n2) 98 | result: [] 99 | - desc: '** Error taking logarithm if the input collection contains multiple items' 100 | expression: Math.arr.log(8) 101 | error: true 102 | - desc: '** Error taking logarithm due to too many input parameters' 103 | expression: Math.n3.log([3, 5]) 104 | error: true 105 | 106 | - desc: '5.7.7 power(exponent : Integer | Decimal) : Integer | Decimal' 107 | - desc: '** Can raise input number to the power of given degree' 108 | expression: Math.n4.power(2) 109 | result: [64] 110 | - desc: '** Empty result if the power cannot be represented' 111 | expression: n6.power(1.5) 112 | result: [] 113 | - desc: '** Empty result if the power cannot be represented' 114 | expression: Math.n6.power(0.5) 115 | result: [] 116 | - desc: '** Empty result when raising empty collection to a power' 117 | expression: Math.n2.power(8) 118 | result: [] 119 | - desc: '** Empty result when raising collection to the empty power' 120 | expression: Math.n3.power(n2) 121 | result: [] 122 | - desc: '** Error raising to a power if the input collection contains multiple items' 123 | expression: Math.arr.power(8) 124 | error: true 125 | - desc: '** Error raising to a power due to too many input parameters' 126 | expression: Math.n3.power([3, 5]) 127 | error: true 128 | 129 | - desc: '5.7.8 round([precision : Integer]) : Decimal' 130 | - desc: '** Can round number with a given accuracy' 131 | expression: Math.d3.round(2) 132 | result: [13.85] 133 | - desc: '** Can round a number to the nearest integer if a given accuracy is empty' 134 | expression: Math.d2.round(n2) 135 | result: [-1] 136 | - desc: '** Empty result when rounding empty number' 137 | expression: Math.n2.round(n3) 138 | result: [] 139 | - desc: '** Error rounding if the input collection contains multiple items' 140 | expression: Math.arr.round(8) 141 | error: true 142 | - desc: '** Error rounding due to too many input parameters' 143 | expression: Math.n3.round([3, 5]) 144 | error: true 145 | 146 | - desc: '5.7.9 sqrt() : Decimal' 147 | - desc: '** Can take square root' 148 | expression: Math.n5.sqrt() 149 | result: [4] 150 | - desc: '** Empty result when taking square root of a negative number' 151 | expression: Math.d2.sqrt() 152 | result: [] 153 | - desc: '** Empty result when taking square root of an empty collection' 154 | expression: Math.n2.sqrt() 155 | result: [] 156 | - desc: '** Error taking square root due to too many input parameters' 157 | expression: Math.n3.sqrt(n4) 158 | error: true 159 | - desc: '** Error taking square root if the input collection contains multiple items' 160 | expression: Math.arr.sqrt() 161 | error: true 162 | 163 | - desc: '5.7.10 truncate() : Integer' 164 | - desc: '** Can return the integer part of the number' 165 | expression: Math.d1.truncate() 166 | result: [1] 167 | - desc: '** Empty result when taking integer part from empty collection' 168 | expression: Math.n2.truncate() 169 | result: [] 170 | - desc: '** Error taking integer part due to too many input parameters' 171 | expression: Math.n3.truncate(n4) 172 | error: true 173 | - desc: '** Error taking integer part if the input collection contains multiple items' 174 | expression: Math.arr.truncate() 175 | error: true 176 | 177 | subject: 178 | resourceType: Math 179 | n0: 0 180 | n1: 1 181 | n2: [] 182 | n3: 2 183 | n4: 8 184 | n5: 16 185 | n6: -1 186 | d1: 1.1 187 | d2: -1.1 188 | d3: 13.84512 189 | arr: [3, 5] 190 | quan: [4.5 'mg'] 191 | 192 | -------------------------------------------------------------------------------- /tests/cases/5.8_tree_navigation.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - desc: '5. Functions' 3 | - desc: '5.7. Tree navigation' 4 | - desc: '5.7.1. children() : collection' 5 | # Returns a collection with all immediate child nodes of all items in the input collection. Note that the ordering of the children is undefined and using operations like first() on the result may return different results on different platforms. 6 | 7 | - desc: '** children' 8 | expression: ch.children() 9 | result: [{d: 1},{e: 1},{d: 2}, {e: 2}] 10 | 11 | - desc: '5.7.2. descendants() : collection' 12 | # Returns a collection with all descendant nodes of all items in the input collection. The result does not include the nodes in the input collection themselves. Is a shorthand for repeat(children()). Note that the ordering of the children is undefined and using operations like first() on the result may return different results on different platforms. 13 | # Note: Many of these functions will result in a set of nodes of different underlying types. It may be necessary to use ofType() as described in the previous section to maintain type safety. See section 8 for more information about type safe use of FHIRPath expressions. 14 | 15 | - desc: '** descendants' 16 | expression: ch.descendants() 17 | result: [{d: 1},{e: 1},{d: 2}, {e: 2}, 1, 1, 2, 2] 18 | - desc: '** descendants' 19 | expression: desc.descendants() 20 | result: [{b: {c: {d: 1}}}, {c: {d: 1}}, {d: 1}, 1] 21 | 22 | 23 | subject: 24 | desc: 25 | a: 26 | b: 27 | c: 28 | d: 29 | 1 30 | ch: 31 | - a: 32 | d: 1 33 | b: 34 | e: 1 35 | - a: 36 | d: 2 37 | b: 38 | e: 2 39 | -------------------------------------------------------------------------------- /tests/cases/5.9_utility_functions.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - 'group: 5. Functions': 3 | - 'group: 5.9. Utility functions': 4 | - 'group: 5.9.1. trace(name : string) : collection': 5 | # Add a string representation of the input collection to the diagnostic log, using the parameter name as the name in the log. This log should be made available to the user in some appropriate fashion. Does not change the input, so returns the input collection as output. 6 | 7 | - desc: '** trace' 8 | expression: coll.attr.trace('coll') 9 | disableConsoleLog: true 10 | result: [1, 2, 3, 4, 5, 6] 11 | 12 | - 'group: 5.9.2. Current date and time functions': 13 | - 'group: now(): DateTime': 14 | 15 | - desc: '** should return the same date as today()' 16 | expression: now().toString().substring(0,10) = today().toString() 17 | result: 18 | - true 19 | 20 | - desc: '** should return the same time as timeOfDay()' 21 | expression: now().toString().substring(11,12) = timeOfDay().toString() 22 | result: 23 | - true 24 | 25 | - 'group: timeOfDay(): Time': 26 | 27 | - desc: '** should return the full time' 28 | expression: timeOfDay().toString().length() = 12 29 | result: 30 | - true 31 | 32 | # TODO: While we don't have a standard method for json serializing Date/Time/DateTime 33 | # objects those utility functions return string representation instead. 34 | # Otherwise we would have to use .toString() each time we access 'now()'. 35 | # Return typechecks back once we decide on how to deal with json serialization. 36 | # 37 | # - desc: '** should return type System.Time' 38 | # expression: timeOfDay().is(System.Time) 39 | # result: 40 | # - true 41 | 42 | subject: 43 | coll: 44 | - attr: [1,2,3] 45 | - attr: [4,5,6] 46 | test: 1 47 | -------------------------------------------------------------------------------- /tests/cases/6.4_collection.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - desc: '6.4. Collections' 3 | - desc: '6.4.1. | (union collections)' 4 | # Merge the two collections into a single collection, eliminating any duplicate values (using equals (=)) to determine equality). Unioning an empty collection to a non-empty collection will return the non-empty collection with duplicates eliminated. There is no expectation of order in the resulting collection. 5 | - desc: 'see 5.4.1 tests' 6 | 7 | - desc: '6.4.2. in (membership)' 8 | # If the left operand is a collection with a single item, this operator returns true if the item is in the right operand using equality semantics. If the left-hand side of the operator is the empty collection is empty, the result is empty, if the right-hand side is empty, the result is false. If the left operand has multiple items, an exception is thrown. 9 | - desc: '** item in coll' 10 | expression: b in c 11 | disable: true 12 | result: [true] 13 | 14 | - desc: '** item not in coll' 15 | expression: a in c 16 | disable: true 17 | result: [false] 18 | 19 | - desc: '** in empty coll' 20 | expression: d in c 21 | disable: true 22 | result: [] 23 | 24 | - desc: '** in empty coll 2' 25 | disable: true 26 | expression: a in d 27 | result: [false] 28 | 29 | - desc: '** in operand is coll' 30 | expression: c in d 31 | disable: true 32 | error: true 33 | 34 | - desc: '6.4.3. contains (containership)' 35 | # If the right operand is a collection with a single item, this operator returns true if the item is in the left operand using equality semantics. This is the inverse operation of in. 36 | - desc: 'see 5.6.5. tests' 37 | 38 | 39 | subject: 40 | - a: 1 41 | b: 2 42 | c: 43 | - 2 44 | - 3 45 | - 4 46 | d: [] 47 | -------------------------------------------------------------------------------- /tests/cases/6.4_collections.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - desc: '6.4.2 |' 3 | - expression: 'coll | coll2' 4 | result: 5 | - a: 1 6 | - a: 2 7 | - a: 3 8 | - a: 4 9 | - a: 5 10 | - desc: '6.4.2 in' 11 | - expression: 'el in coll' 12 | result: [true] 13 | - expression: 'not_el in coll' 14 | result: [false] 15 | - expression: 'ups in coll' 16 | desc: 'If the left-hand side of the operator is the empty collection is empty, the result is empty' 17 | result: [] 18 | - expression: 'el in emptycoll' 19 | desc: 'if the right-hand side is empty, the result is false' 20 | result: [false] 21 | - expression: 'ups in emptycoll' 22 | desc: 'empty in empty is empty' 23 | result: [] 24 | - expression: 'coll in coll' 25 | desc: 'If the left operand has multiple items, an exception is thrown' 26 | error: true 27 | - desc: '6.4.3 contains' 28 | - expression: 'coll contains el' 29 | result: [true] 30 | - expression: 'coll contains not_el' 31 | result: [false] 32 | - expression: 'coll contains empty' 33 | result: [] 34 | - expression: 'coll contains ups' 35 | desc: 'reverse of: If the left-hand side of the operator is the empty collection is empty, the result is empty' 36 | result: [] 37 | - expression: 'emptycoll contains el' 38 | desc: 'reverse of: if the right-hand side is empty, the result is false' 39 | result: [false] 40 | - expression: 'emptycoll contains ups' 41 | desc: 'empty in empty is empty' 42 | result: [] 43 | - expression: 'coll contains coll' 44 | desc: 'reverse of: If the left operand has multiple items, an exception is thrown' 45 | error: true 46 | subject: 47 | el: 48 | a: 2 49 | not_el: 50 | a: 4 51 | coll2: 52 | - a: 3 53 | - a: 4 54 | - a: 5 55 | coll: 56 | - a: 1 57 | - a: 2 58 | - a: 3 59 | emptycoll: [] 60 | -------------------------------------------------------------------------------- /tests/cases/6.5_boolean_logic.yaml: -------------------------------------------------------------------------------- 1 | # Tests for 6.5 Comparisons 2 | 3 | tests: 4 | - desc: 6.5. Boolean logic 5 | - desc: '* 6.5.1 and' 6 | - desc: '* and (true and true)' 7 | expression: ok1 and ok2 8 | result: [true] 9 | - desc: '* and (true and false)' 10 | expression: ok1 and ups1 11 | result: [false] 12 | - desc: '* and (true and false)' 13 | expression: ok1 and ups1 14 | result: [false] 15 | - desc: '* and (false and false)' 16 | expression: ok1 and ups1 17 | result: [false] 18 | - desc: '* Empty logic' 19 | - desc: '* and ({} and true)' 20 | expression: emp and ok1 21 | result: [] 22 | - desc: '* and (true and {})' 23 | expression: ok1 and emp 24 | result: [] 25 | - desc: '* and ({} and false)' 26 | expression: emp and ups1 27 | result: [false] 28 | - desc: '* and (false and {})' 29 | expression: ups1 and emp 30 | result: [false] 31 | 32 | - desc: '* 6.5.2 or' 33 | - desc: '* or (true and true)' 34 | expression: ok1 or ok2 35 | result: [true] 36 | - desc: '* or (true or false)' 37 | expression: ok1 or ups1 38 | result: [true] 39 | - desc: '* or (false or false)' 40 | expression: ups2 or ups1 41 | result: [false] 42 | 43 | - desc: '* Empty logic' 44 | - desc: '* or ({} or true)' 45 | expression: emp or ok1 46 | result: [true] 47 | - desc: '* or (true or {})' 48 | expression: ok1 or emp 49 | result: [true] 50 | - desc: '* or ({} or false)' 51 | expression: emp or ups1 52 | result: [] 53 | - desc: '* or (false or {})' 54 | expression: ups1 or emp 55 | result: [] 56 | - desc: '* or ({} or {})' 57 | expression: emp or emp 58 | result: [] 59 | 60 | - desc: '* 6.5.3 not' 61 | - desc: '** not() for non-empty collection' 62 | inputfile: patient-example.json 63 | expression: (0).not() 64 | result: 65 | - false 66 | 67 | - desc: '* 6.5.4 xor' 68 | - desc: '* xor (true xor true)' 69 | expression: ok1 xor ok2 70 | result: [false] 71 | - desc: '* xor (true xor false)' 72 | expression: ok1 xor ups1 73 | result: [true] 74 | - desc: '* xor (false xor true)' 75 | expression: ups1 xor ok1 76 | result: [true] 77 | - desc: '* or (false or false)' 78 | expression: ups2 xor ups1 79 | result: [false] 80 | 81 | - desc: '* Empty logic' 82 | - desc: '* xor ({} xor true)' 83 | expression: emp xor ok1 84 | result: [] 85 | - desc: '* xor (true xor {})' 86 | expression: ok1 xor emp 87 | result: [] 88 | - desc: '* xor ({} xor false)' 89 | expression: emp xor ups1 90 | result: [] 91 | - desc: '* xor (false xor {})' 92 | expression: ups1 xor emp 93 | result: [] 94 | - desc: '* xor ({} xor {})' 95 | expression: emp xor emp 96 | result: [] 97 | 98 | 99 | - desc: '* 6.5.5 implies' 100 | - desc: '* implies (true implies true)' 101 | expression: ok1 implies ok2 102 | result: [true] 103 | - desc: '* implies (true implies false)' 104 | expression: ok1 implies ups1 105 | result: [false] 106 | - desc: '* implies (false implies true)' 107 | expression: ups1 implies ok1 108 | result: [true] 109 | - desc: '* implies (false implies false)' 110 | expression: ups2 implies ups1 111 | result: [true] 112 | 113 | - desc: '* Empty logic' 114 | - desc: '* implies ({} implies true)' 115 | expression: emp implies ok1 116 | result: [true] 117 | - desc: '* implies (true implies {})' 118 | expression: ok1 implies emp 119 | result: [] 120 | - desc: '* implies ({} implies false)' 121 | expression: emp implies ups1 122 | result: [] 123 | - desc: '* implies (false implies {})' 124 | expression: ups1 implies emp 125 | result: [true] 126 | - desc: '* implies ({} implies {})' 127 | expression: emp implies emp 128 | result: [] 129 | 130 | - desc: '* in where TODO:' 131 | # expression: coll.where(a='a' and b='b') 132 | # result: 133 | # - a: a 134 | # b: b 135 | subject: 136 | ok1: true 137 | ok2: true 138 | ups1: false 139 | ups2: false 140 | emp: [] 141 | coll: 142 | - a: a 143 | b: b 144 | - a: b 145 | b: a 146 | - a: c 147 | b: d 148 | -------------------------------------------------------------------------------- /tests/cases/7_aggregate.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - 'group: 7 Aggregates': 3 | - 'group: 7.1 aggregate(aggregator : expression [, init : value]) : value': 4 | - desc: 'aggregate() function within the aggregate() iterator' 5 | expression: (1|2|3).aggregate($total + (1|2|3|4).aggregate($total + $this, 0), 0) = 30 6 | result: 7 | - true 8 | - desc: 'Calculate sum of undefined input collection, start with 3' 9 | expression: UndefinedInput.aggregate($total + $this, 3) = 3 10 | result: 11 | - true 12 | - desc: 'Using $index in an expression for the aggregate function' 13 | expression: (10|20|30|0).aggregate($total + $index, 0) = 6 14 | result: 15 | - true 16 | - desc: 'Using the result of the aggregate function' 17 | expression: 10 + (1|2|3).aggregate($total + $this*$index, 4) = 22 18 | result: 19 | - true 20 | - desc: 'aggregate() function with string initial value' 21 | expression: ('a'|'b'|'c').aggregate($total & '-' & $this, 'concat') 22 | result: 23 | - 'concat-a-b-c' 24 | - 'group: Extension functions': 25 | - desc: 'sum() function calculates the sum of input collection' 26 | expression: (1|2|3|4|5|6|7|8|9).sum() = 45 27 | result: 28 | - true 29 | - desc: 'sum() function calculates the sum of undefined input collection' 30 | expression: UndefinedInput.sum() = 0 31 | result: 32 | - true 33 | - desc: 'min() function returns the minimum value from the input collection' 34 | expression: (7|8|9|1|2|3|4|5|6).min() = 1 35 | result: 36 | - true 37 | - desc: 'min() function returns empty value for the undefined input collection' 38 | expression: UndefinedInput.min() 39 | result: [] 40 | - desc: 'max() function returns the maximum value from the input collection' 41 | expression: (7|8|9|1|2|3|4|5|6).max() = 9 42 | result: 43 | - true 44 | - desc: 'max() function returns empty value for the undefined input collection' 45 | expression: UndefinedInput.max() 46 | result: [] 47 | - desc: 'avg() function calculates the average value for the input collection' 48 | expression: (7|8|9|1|2|3|4|5|6).avg() = 5 49 | result: 50 | - true 51 | - desc: 'avg() function returns an empty value for the undefined input collection' 52 | expression: UndefinedInput.avg() 53 | result: [] 54 | -------------------------------------------------------------------------------- /tests/cases/8_variables.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - 'group: Environment variables': 3 | - 'group: Standard variables': 4 | - desc: '%resource (FHIR extension)' 5 | expression: '%resource.n1' 6 | result: [1] 7 | - desc: '%context' 8 | expression: '%context.n1' 9 | result: [1] 10 | - desc: '%context' 11 | context: 'g1' 12 | expression: '%context.n1' 13 | result: [2] 14 | - desc: '%ucum' 15 | expression: '%ucum' 16 | result: ['http://unitsofmeasure.org'] 17 | - expression: '%a - 1' 18 | variables: 19 | a: 5 20 | result: [4] 21 | - desc: 'Empty variable' 22 | expression: '%a' 23 | variables: 24 | a: [] 25 | result: [] 26 | - desc: 'Null variable' 27 | expression: '%a' 28 | variables: 29 | a: null 30 | result: [] 31 | - desc: 'Undefined variable' 32 | expression: '%a' 33 | error: true 34 | - desc: 'Escaped variables' 35 | expression: '%`a.b() - 1` - 2' 36 | variables: 37 | "a.b() - 1": 5 38 | result: [3] 39 | 40 | subject: 41 | n1: 1 42 | g1: 43 | n1: 2 44 | -------------------------------------------------------------------------------- /tests/cases/extensions.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | # https://www.hl7.org/fhir/fhirpath.html#types 3 | - 'group: Extension and id for primitive types': 4 | 5 | - desc: '** id for primitive type' 6 | expression: Functions.attrtrue.id = 'someid' 7 | result: 8 | - true 9 | 10 | - desc: '** expression with extension for primitive type 1' 11 | inputfile: patient-example.json 12 | expression: Patient.birthDate.extension.where(url = '').empty() 13 | result: 14 | - true 15 | 16 | - desc: '** expression with extension for primitive type 2' 17 | inputfile: patient-example.json 18 | expression: >- 19 | Patient.birthDate.extension 20 | .where(url = 'http://hl7.org/fhir/StructureDefinition/patient-birthTime') 21 | .valueDateTime.toDateTime() = @1974-12-25T14:35:45-05:00 22 | result: 23 | - true 24 | 25 | - desc: '** expression with extension for primitive type 3' 26 | inputfile: patient-example.json 27 | model: r4 28 | expression: >- 29 | Patient.birthDate.extension 30 | .where(url = 'http://hl7.org/fhir/StructureDefinition/patient-birthTime') 31 | .value = @1974-12-25T14:35:45-05:00 32 | result: 33 | - true 34 | 35 | # https://www.hl7.org/fhir/fhirpath.html#functions 36 | - 'group: Additional functions': 37 | - desc: 'extension(url : string) : collection' 38 | 39 | # If the url is empty ({ }), the result is empty. 40 | - desc: '** empty url' 41 | inputfile: patient-example.json 42 | expression: Patient.birthDate.extension('').empty() 43 | result: 44 | - true 45 | 46 | # If the input collection is empty ({ }), the result is empty. 47 | - desc: '** empty input collection' 48 | inputfile: patient-example.json 49 | expression: >- 50 | Patient.birthDate1 51 | .extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime').empty() 52 | result: 53 | - true 54 | 55 | - desc: '** expression with extension() for primitive type (without using FHIR model data)' 56 | inputfile: patient-example.json 57 | expression: >- 58 | Patient.birthDate.extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime') 59 | .valueDateTime.toDateTime() = @1974-12-25T14:35:45-05:00 60 | result: 61 | - true 62 | 63 | - desc: '** expression with extension() for primitive type (without using FHIR model data) when only extension is present' 64 | inputfile: patient-example-2.json 65 | expression: >- 66 | Patient.communication.preferred.extension('test').exists() 67 | result: 68 | - true 69 | 70 | - desc: '** expression with extension() for primitive type (using FHIR model data) when only extension is present' 71 | inputfile: patient-example-2.json 72 | model: r4 73 | expression: >- 74 | Patient.communication.preferred.extension('test').value.id 75 | result: 76 | - testing 77 | 78 | - desc: '** expression with extension() for primitive type (using FHIR model data)' 79 | inputfile: patient-example.json 80 | model: r4 81 | expression: >- 82 | Patient.birthDate.extension('http://hl7.org/fhir/StructureDefinition/patient-birthTime') 83 | .value = @1974-12-25T14:35:45-05:00 84 | result: 85 | - true 86 | 87 | - desc: '** value of extension of extension (using FHIR model data)' 88 | model: r4 89 | expression: Functions.attrtrue.extension('url1').extension('url2').value = 'someuri' 90 | result: 91 | - true 92 | 93 | - desc: '** id of extension of extension' 94 | expression: Functions.attrtrue.extension('url1').extension('url2').id = 'someid2' 95 | result: 96 | - true 97 | 98 | subject: 99 | resourceType: Functions 100 | attrtrue: true 101 | _attrtrue: 102 | id: someid 103 | extension: 104 | - url: url1 105 | extension: 106 | - url: url2 107 | id: someid2 108 | valueUri: someuri 109 | -------------------------------------------------------------------------------- /tests/cases/fhir-quantity.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - 'group: standard tests from fhir-r4': 3 | - desc: '** test' 4 | inputfile: patient-example.json 5 | expression: 1.toQuantity() = 1 '1' 6 | result: 7 | - true 8 | - desc: '** test' 9 | inputfile: patient-example.json 10 | expression: 1.0.toQuantity() = 1.0 '1' 11 | result: 12 | - true 13 | - desc: '** test' 14 | inputfile: patient-example.json 15 | expression: '''1''.toQuantity()' 16 | result: 17 | - 1 '1' 18 | - desc: '** test' 19 | inputfile: patient-example.json 20 | expression: '''1 day''.toQuantity() = 1 day' 21 | result: 22 | - true 23 | - desc: '** test' 24 | inputfile: patient-example.json 25 | expression: '''1 day''.toQuantity() = 1 ''d''' 26 | result: 27 | - true 28 | - desc: '** test' 29 | inputfile: patient-example.json 30 | expression: '''1 \''wk\''''.toQuantity() = 1 week' 31 | result: 32 | - true 33 | - desc: '** test' 34 | inputfile: patient-example.json 35 | expression: '''1.0''.toQuantity() ~ 1 ''1''' 36 | result: 37 | - true 38 | # see Use of FHIR Quantity: https://www.hl7.org/fhir/fhirpath.html#quantity 39 | - 'group: Mapping from FHIR Quantity to FHIRPath System.Quantity': 40 | - desc: After converting 'a' to year it is equal to year 41 | inputfile: quantity-example.json 42 | model: 'r4' 43 | expression: QuestionnaireResponse.item[0].answer.value = 2 year 44 | result: 45 | - true 46 | - desc: After converting 'a' to year it is equivalent to 'a' 47 | inputfile: quantity-example.json 48 | model: 'r4' 49 | expression: QuestionnaireResponse.item[0].answer.value ~ 2 'a' 50 | result: 51 | - true 52 | - desc: After converting 'a' to year it isn't equal to 'a' 53 | inputfile: quantity-example.json 54 | model: 'r4' 55 | expression: QuestionnaireResponse.item[0].answer.value != 2 'a' 56 | result: 57 | - true 58 | - desc: After converting 'min' to minute it is equal to 'min' 59 | inputfile: quantity-example.json 60 | model: 'r4' 61 | expression: QuestionnaireResponse.item[1].answer.value = 3 'min' 62 | result: 63 | - true 64 | - desc: Unable to convert from non-UCUM system 65 | inputfile: quantity-example.json 66 | model: 'r4' 67 | expression: QuestionnaireResponse.item[2].answer.value.toQuantity() 68 | result: [] 69 | - desc: Error when a comparator is present and there is a need to convert 70 | inputfile: quantity-example.json 71 | model: 'r4' 72 | expression: QuestionnaireResponse.item[3].answer.value.toQuantity() 73 | error: true 74 | - desc: Can access the comparator field when there isn't a need to convert 75 | inputfile: quantity-example.json 76 | model: 'r4' 77 | expression: QuestionnaireResponse.item[3].answer.value.comparator 78 | result: 79 | - '>' 80 | 81 | -------------------------------------------------------------------------------- /tests/cases/simple.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - expression: 3 | - Patient.name.family 4 | - name.family 5 | - name.`family` 6 | result: 7 | - Chalmers 8 | - Windsor 9 | 10 | - desc: mapcat arrays 11 | expression: Patient.name.given 12 | result: ["Peter", "James", "Jim", "Peter", "James"] 13 | 14 | - expression: 15 | - Patient.id 16 | - id 17 | result: ["example"] 18 | 19 | - expression: 20 | - Encounter.id 21 | result: [] 22 | 23 | - desc: access by index 24 | expression: Patient.address[0].use 25 | result: ["home"] 26 | 27 | - desc: access by index 28 | expression: Patient.name[0].family 29 | result: ["Chalmers"] 30 | 31 | - desc: access by index unexisting 32 | expression: Patient.name[1].family 33 | result: [] 34 | 35 | - desc: access by index 36 | expression: Patient.name[2].family 37 | result: ["Windsor"] 38 | 39 | - desc: access by index unexisting 40 | expression: Patient.name[3].family 41 | result: [] 42 | 43 | - desc: access by index 44 | expression: Patient.name[1].given[0] 45 | result: ["Jim"] 46 | 47 | 48 | - expression: Patient.managingOrganization.reference 49 | result: ["Organization/1"] 50 | 51 | - expression: Patient.name.exists() 52 | result: [true] 53 | 54 | - expression: Patient.name.exists(given) 55 | result: [true] 56 | 57 | - expression: Patient.ups.exists() 58 | result: [false] 59 | 60 | - expression: Patient.name.empty() 61 | result: [false] 62 | 63 | - expression: Patient.ups.empty() 64 | result: [true] 65 | 66 | - desc: count 67 | expression: Patient.name.given.count() 68 | result: [5] 69 | 70 | - desc: count 71 | expression: Patient.name.given.count() = 5 72 | result: [true] 73 | 74 | - expression: Patient.name.where(use ='official').family 75 | result: ["Chalmers"] 76 | 77 | - expression: Patient.name.where(use ='official').given 78 | result: ["Peter", "James"] 79 | 80 | - expression: "'a' | 'b'" 81 | result: ["a", "b"] 82 | 83 | - expression: Patient.name.select(given) 84 | result: ["Peter", "James", "Jim", "Peter", "James"] 85 | 86 | - expression: Patient.name.given | Patient.name.family 87 | result: ["Peter", "James", "Jim", "Chalmers", "Windsor"] 88 | 89 | - expression: Patient.name.select(given | family) 90 | result: ["Peter", "James", "Chalmers", "Jim", "Peter", "James", "Windsor"] 91 | 92 | - expression: Patient.name.select(given.union(family)) 93 | result: ["Peter", "James", "Chalmers", "Jim", "Peter", "James", "Windsor"] 94 | 95 | - expression: Patient.name.select(given.combine(family)) 96 | result: ["Peter", "James", "Chalmers", "Jim", "Peter", "James", "Windsor"] 97 | 98 | - expression: Patient.name.select(('James').subsetOf(given)) 99 | result: [true, false, true] 100 | 101 | - expression: Patient.name.select(('Peter' | 'James').supersetOf(given)) 102 | result: [true, false, true] 103 | 104 | - expression: Patient.name.select(('Peter' | 'James' | 'something').intersect(given | family)) 105 | result: ["Peter", "James", "Peter", "James"] 106 | 107 | - expression: Patient.name.trace('tracing').given[0] 108 | result: ["Peter"] 109 | disableConsoleLog: true 110 | 111 | # $this 112 | - expression: Patient.name.given.select($this.single()) 113 | result: ["Peter", "James", "Jim", "Peter", "James"] 114 | 115 | - expression: Patient.name.given.select($this.contains('Ja')) 116 | result: [false, true, false, false, true] 117 | 118 | - expression: Patient.name.given.select($this.length) = Patient.name.given.select(length) 119 | result: [true] 120 | 121 | - expression: Patient.telecom.where($this.use = 'work') 122 | result: [{"use": "work", "rank": 1, "value": "(03) 5555 6473", "system": "phone"}] 123 | 124 | - expression: identifier.where($this.type.coding.code = 'MR').value 125 | result: ['12345'] 126 | 127 | - expression: identifier.where($this.type.coding.code = 'Z').value 128 | result: [] 129 | 130 | - expression: (2).combine(3) 131 | desc: 'Parenthesized expression' 132 | result: [2,3] 133 | 134 | - desc: Evaluating expression for a part of a resource 135 | inputfile: questionnaire-part-example.json 136 | model: 'r4' 137 | expression: {base: 'QuestionnaireResponse.item', expression: 'answer.value = 2 year'} 138 | result: 139 | - true 140 | 141 | 142 | 143 | subject: 144 | resourceType: Patient 145 | id: example 146 | address: 147 | - use: home 148 | city: PleasantVille 149 | type: both 150 | state: Vic 151 | line: 152 | - 534 Erewhon St 153 | postalCode: '3999' 154 | period: 155 | start: '1974-12-25' 156 | district: Rainbow 157 | text: '534 Erewhon St PeasantVille, Rainbow, Vic 3999' 158 | managingOrganization: 159 | reference: Organization/1 160 | name: 161 | - use: official 162 | given: 163 | - Peter 164 | - James 165 | family: Chalmers 166 | - use: usual 167 | given: 168 | - Jim 169 | - use: maiden 170 | given: 171 | - Peter 172 | - James 173 | family: Windsor 174 | period: 175 | end: '2002' 176 | birthDate: '1974-12-25' 177 | deceased: 178 | boolean: false 179 | active: true 180 | identifier: 181 | - use: usual 182 | type: 183 | coding: 184 | - code: MR 185 | system: 'http://hl7.org/fhir/v2/0203' 186 | value: '12345' 187 | period: 188 | start: '2001-05-06' 189 | system: 'urn:oid:1.2.36.146.595.217.0.1' 190 | assigner: 191 | display: Acme Healthcare 192 | telecom: 193 | - use: home 194 | - use: work 195 | rank: 1 196 | value: (03) 5555 6473 197 | system: phone 198 | - use: mobile 199 | rank: 2 200 | value: (03) 3410 5613 201 | system: phone 202 | - use: old 203 | value: (03) 5555 8834 204 | period: 205 | end: '2014' 206 | system: phone 207 | gender: male 208 | contact: 209 | - name: 210 | given: 211 | - Bénédicte 212 | family: du Marché 213 | _family: 214 | extension: 215 | - url: 'http://hl7.org/fhir/StructureDefinition/humanname-own-prefix' 216 | valueString: VV 217 | gender: female 218 | period: 219 | start: '2012' 220 | address: 221 | use: home 222 | city: PleasantVille 223 | line: 224 | - 534 Erewhon St 225 | type: both 226 | state: Vic 227 | period: 228 | start: '1974-12-25' 229 | district: Rainbow 230 | postalCode: '3999' 231 | telecom: 232 | - value: +33 (237) 998327 233 | system: phone 234 | relationship: 235 | - coding: 236 | - code: 'N' 237 | system: 'http://hl7.org/fhir/v2/0131' 238 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import yaml 3 | 4 | from fhirpathpy import evaluate 5 | from fhirpathpy.engine.nodes import FP_Quantity 6 | from fhirpathpy.models import models 7 | from tests.resources import resources 8 | 9 | 10 | def pytest_collect_file(parent, path): 11 | if path.ext == ".yaml": 12 | return YamlFile.from_parent(parent, fspath=path) 13 | 14 | 15 | class YamlFile(pytest.File): 16 | def collect(self): 17 | raw = yaml.safe_load(self.fspath.open()) 18 | 19 | suites = raw["tests"] 20 | subject = raw["subject"] if "subject" in raw else None 21 | 22 | return self.collect_tests(suites, subject) 23 | 24 | def is_group(self, test): 25 | if not isinstance(test, dict): 26 | return False 27 | 28 | return any(key.startswith("group") for key in test.keys()) 29 | 30 | def collect_tests(self, suites, subject, is_group_disabled=False): 31 | for suite in suites: 32 | current_group_disabled = is_group_disabled or suite.get("disable", False) 33 | if self.is_group(suite): 34 | name = next(iter(suite)) 35 | tests = suite[name] 36 | for test in self.collect_tests(tests, subject, current_group_disabled): 37 | yield test 38 | else: 39 | for test in self.collect_test(suite, subject, current_group_disabled): 40 | yield test 41 | 42 | def collect_test(self, test, subject, is_group_disabled): 43 | name = test["desc"] if "desc" in test else "" 44 | is_disabled = ( 45 | is_group_disabled if is_group_disabled else "disable" in test and test["disable"] 46 | ) 47 | 48 | if "expression" in test and not is_disabled: 49 | if isinstance(test["expression"], list): 50 | for expression in test["expression"]: 51 | test["expression"] = expression 52 | yield YamlItem.from_parent( 53 | self, 54 | name=name, 55 | test=test, 56 | resource=subject, 57 | ) 58 | else: 59 | yield YamlItem.from_parent( 60 | self, 61 | name=name, 62 | test=test, 63 | resource=subject, 64 | ) 65 | 66 | 67 | class YamlItem(pytest.Item): 68 | def __init__(self, name, parent, test, resource=None): 69 | super().__init__(name, parent) 70 | 71 | self.test = test 72 | self.resource = resource 73 | 74 | def runtest(self): 75 | expression = self.test["expression"] 76 | resource = self.resource 77 | 78 | model = models[self.test["model"]] if "model" in self.test else None 79 | 80 | if "inputfile" in self.test: 81 | if self.test["inputfile"] in resources: 82 | resource = resources[self.test["inputfile"]] 83 | 84 | variables = {"resource": resource} 85 | 86 | if "context" in self.test: 87 | variables["context"] = evaluate(resource, self.test["context"])[0] 88 | 89 | if "variables" in self.test: 90 | variables.update(self.test["variables"]) 91 | 92 | if "error" in self.test and self.test["error"] is True: 93 | with pytest.raises(Exception) as exc: 94 | raise Exception(self.test["desc"]) from exc 95 | else: 96 | result = evaluate(resource, expression, variables, model) 97 | compare(result, self.test["result"]) 98 | 99 | 100 | def compare(l1, l2): 101 | # TODO REFACTOR 102 | if l1 == l2: 103 | assert True 104 | elif len(l1) == len(l2) == 1: 105 | e1 = l1[0] 106 | e2 = evaluate({}, l2[0])[0] if isinstance(l2[0], str) else l2[0] 107 | if isinstance(e1, FP_Quantity) and isinstance(e2, FP_Quantity): 108 | assert e1 == e2 109 | else: 110 | assert str(e1) == str(e2) 111 | else: 112 | raise AssertionError(f"{l1} != {l2}") 113 | -------------------------------------------------------------------------------- /tests/fixtures/ast/%v+2.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [{ 3 | "type": "AdditiveExpression", 4 | "terminalNodeText": ["+"], 5 | "children": [{ 6 | "type": "TermExpression", 7 | "text": "%v", 8 | "terminalNodeText": [], 9 | "children": [{ 10 | "type": "ExternalConstantTerm", 11 | "terminalNodeText": [], 12 | "children": [{ 13 | "type": "ExternalConstant", 14 | "terminalNodeText": ["%"], 15 | "children": [{ 16 | "type": "Identifier", 17 | "text": "v", 18 | "terminalNodeText": ["v"] 19 | }] 20 | }] 21 | }] 22 | }, { 23 | "type": "TermExpression", 24 | "text": "2", 25 | "terminalNodeText": [], 26 | "children": [{ 27 | "type": "LiteralTerm", 28 | "text": "2", 29 | "terminalNodeText": [], 30 | "children": [{ 31 | "type": "NumberLiteral", 32 | "text": "2", 33 | "terminalNodeText": ["2"] 34 | }] 35 | }] 36 | }] 37 | }] 38 | } -------------------------------------------------------------------------------- /tests/fixtures/ast/Observation.value.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [{ 3 | "type": "InvocationExpression", 4 | "text": "Observation.value", 5 | "terminalNodeText": ["."], 6 | "children": [{ 7 | "type": "TermExpression", 8 | "text": "Observation", 9 | "terminalNodeText": [], 10 | "children": [{ 11 | "type": "InvocationTerm", 12 | "terminalNodeText": [], 13 | "children": [{ 14 | "type": "MemberInvocation", 15 | "terminalNodeText": [], 16 | "children": [{ 17 | "type": "Identifier", 18 | "text": "Observation", 19 | "terminalNodeText": ["Observation"] 20 | }] 21 | }] 22 | }] 23 | }, { 24 | "type": "MemberInvocation", 25 | "terminalNodeText": [], 26 | "children": [{ 27 | "type": "Identifier", 28 | "text": "value", 29 | "terminalNodeText": ["value"] 30 | }] 31 | }] 32 | }] 33 | } -------------------------------------------------------------------------------- /tests/fixtures/ast/Patient.name.given.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [{ 3 | "type": "InvocationExpression", 4 | "text": "Patient.name.given", 5 | "terminalNodeText": ["."], 6 | "children": [{ 7 | "type": "InvocationExpression", 8 | "text": "Patient.name", 9 | "terminalNodeText": ["."], 10 | "children": [{ 11 | "type": "TermExpression", 12 | "text": "Patient", 13 | "terminalNodeText": [], 14 | "children": [{ 15 | "type": "InvocationTerm", 16 | "terminalNodeText": [], 17 | "children": [{ 18 | "type": "MemberInvocation", 19 | "terminalNodeText": [], 20 | "children": [{ 21 | "type": "Identifier", 22 | "text": "Patient", 23 | "terminalNodeText": ["Patient"] 24 | }] 25 | }] 26 | }] 27 | }, { 28 | "type": "MemberInvocation", 29 | "terminalNodeText": [], 30 | "children": [{ 31 | "type": "Identifier", 32 | "text": "name", 33 | "terminalNodeText": ["name"] 34 | }] 35 | }] 36 | }, { 37 | "type": "MemberInvocation", 38 | "terminalNodeText": [], 39 | "children": [{ 40 | "type": "Identifier", 41 | "text": "given", 42 | "terminalNodeText": ["given"] 43 | }] 44 | }] 45 | }] 46 | } -------------------------------------------------------------------------------- /tests/fixtures/ast/a.b+2.json: -------------------------------------------------------------------------------- 1 | { 2 | "children": [{ 3 | "type": "AdditiveExpression", 4 | "terminalNodeText": ["+"], 5 | "children": [{ 6 | "type": "InvocationExpression", 7 | "text": "a.b", 8 | "terminalNodeText": ["."], 9 | "children": [{ 10 | "type": "TermExpression", 11 | "text": "a", 12 | "terminalNodeText": [], 13 | "children": [{ 14 | "type": "InvocationTerm", 15 | "terminalNodeText": [], 16 | "children": [{ 17 | "type": "MemberInvocation", 18 | "terminalNodeText": [], 19 | "children": [{ 20 | "type": "Identifier", 21 | "text": "a", 22 | "terminalNodeText": ["a"] 23 | }] 24 | }] 25 | }] 26 | }, { 27 | "type": "MemberInvocation", 28 | "terminalNodeText": [], 29 | "children": [{ 30 | "type": "Identifier", 31 | "text": "b", 32 | "terminalNodeText": ["b"] 33 | }] 34 | }] 35 | }, { 36 | "type": "TermExpression", 37 | "text": "2", 38 | "terminalNodeText": [], 39 | "children": [{ 40 | "type": "LiteralTerm", 41 | "text": "2", 42 | "terminalNodeText": [], 43 | "children": [{ 44 | "type": "NumberLiteral", 45 | "text": "2", 46 | "terminalNodeText": ["2"] 47 | }] 48 | }] 49 | }] 50 | }] 51 | } -------------------------------------------------------------------------------- /tests/resources/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pathlib 4 | 5 | current_dir = pathlib.Path(__file__).parent.resolve() 6 | 7 | resources = {} 8 | 9 | 10 | files = [f for f in os.listdir(current_dir)] 11 | for f in files: 12 | fp = os.path.join(current_dir, f) 13 | print(fp) 14 | if os.path.isfile(fp) and f.endswith(".json"): 15 | with open(fp) as fd: 16 | resources[f] = json.loads(fd.read()) 17 | -------------------------------------------------------------------------------- /tests/resources/medicationrequest-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "MedicationRequest", 3 | "dispenseRequest": { 4 | "expectedSupplyDuration": { 5 | "value": 3, 6 | "unit": "days", 7 | "system": "http://unitsofmeasure.org", 8 | "code": "d" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/resources/observation-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: example

status: final

category: Vital Signs (Details : {http://terminology.hl7.org/CodeSystem/observation-category code 'vital-signs' = 'Vital Signs', given as 'Vital Signs'})

code: Body Weight (Details : {LOINC code '29463-7' = 'Body weight', given as 'Body Weight'}; {LOINC code '3141-9' = 'Body weight Measured', given as 'Body weight Measured'}; {SNOMED CT code '27113001' = 'Body weight', given as 'Body weight'}; {http://acme.org/devices/clinical-codes code 'body-weight' = 'body-weight', given as 'Body Weight'})

subject: Patient/example

encounter: Encounter/example

effective: 28/03/2016

value: 185 lbs (Details: UCUM code [lb_av] = 'lb_av')

" 7 | }, 8 | "status": "final", 9 | "category": [ 10 | { 11 | "coding": [ 12 | { 13 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 14 | "code": "vital-signs", 15 | "display": "Vital Signs" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "29463-7", 25 | "display": "Body Weight" 26 | }, 27 | { 28 | "system": "http://loinc.org", 29 | "code": "3141-9", 30 | "display": "Body weight Measured" 31 | }, 32 | { 33 | "system": "http://snomed.info/sct", 34 | "code": "27113001", 35 | "display": "Body weight" 36 | }, 37 | { 38 | "system": "http://acme.org/devices/clinical-codes", 39 | "code": "body-weight", 40 | "display": "Body Weight" 41 | } 42 | ] 43 | }, 44 | "subject": { 45 | "reference": "Patient/example" 46 | }, 47 | "encounter": { 48 | "reference": "Encounter/example" 49 | }, 50 | "effectiveDateTime": "2016-03-28", 51 | "valueQuantity": { 52 | "value": 185, 53 | "unit": "lbs", 54 | "system": "http://unitsofmeasure.org", 55 | "code": "[lb_av]" 56 | } 57 | } -------------------------------------------------------------------------------- /tests/resources/patient-example-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "communication": [ 4 | { 5 | "language": { 6 | "coding": [ 7 | { 8 | "system": "urn:ietf:bcp:47", 9 | "code": "nl", 10 | "display": "Dutch" 11 | } 12 | ] 13 | }, 14 | "_preferred": { 15 | "extension": [ 16 | { 17 | "url": "test", 18 | "_valueString": { 19 | "id": "testing" 20 | } 21 | } 22 | ] 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/resources/patient-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "example", 4 | "text": { 5 | "status": "generated", 6 | "div": "
NamePeter James \n Chalmers (\"Jim\")\n
Address534 Erewhon, Pleasantville, Vic, 3999
ContactsHome: unknown. Work: (03) 5555 6473
IdMRN: 12345 (Acme Healthcare)
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "usual", 11 | "type": { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203", 15 | "code": "MR" 16 | } 17 | ] 18 | }, 19 | "system": "urn:oid:1.2.36.146.595.217.0.1", 20 | "value": "12345", 21 | "period": { 22 | "start": "2001-05-06" 23 | }, 24 | "assigner": { 25 | "display": "Acme Healthcare" 26 | } 27 | } 28 | ], 29 | "active": true, 30 | "name": [ 31 | { 32 | "use": "official", 33 | "family": "Chalmers", 34 | "given": [ 35 | "Peter", 36 | "James" 37 | ] 38 | }, 39 | { 40 | "use": "usual", 41 | "given": [ 42 | "Jim" 43 | ] 44 | }, 45 | { 46 | "use": "maiden", 47 | "family": "Windsor", 48 | "given": [ 49 | "Peter", 50 | "James" 51 | ], 52 | "period": { 53 | "end": "2002" 54 | } 55 | } 56 | ], 57 | "telecom": [ 58 | { 59 | "use": "home" 60 | }, 61 | { 62 | "system": "phone", 63 | "value": "(03) 5555 6473", 64 | "use": "work", 65 | "rank": 1 66 | }, 67 | { 68 | "system": "phone", 69 | "value": "(03) 3410 5613", 70 | "use": "mobile", 71 | "rank": 2 72 | }, 73 | { 74 | "system": "phone", 75 | "value": "(03) 5555 8834", 76 | "use": "old", 77 | "period": { 78 | "end": "2014" 79 | } 80 | } 81 | ], 82 | "gender": "male", 83 | "_birthDate": { 84 | "extension": [ 85 | { 86 | "url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime", 87 | "valueDateTime": "1974-12-25T14:35:45-05:00" 88 | } 89 | ] 90 | }, 91 | "birthDate": "1974-12-25", 92 | "deceasedBoolean": false, 93 | "address": [ 94 | { 95 | "use": "home", 96 | "type": "both", 97 | "text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999", 98 | "line": [ 99 | "534 Erewhon St" 100 | ], 101 | "city": "PleasantVille", 102 | "district": "Rainbow", 103 | "state": "Vic", 104 | "postalCode": "3999", 105 | "period": { 106 | "start": "1974-12-25" 107 | } 108 | } 109 | ], 110 | "contact": [ 111 | { 112 | "relationship": [ 113 | { 114 | "coding": [ 115 | { 116 | "system": "http://terminology.hl7.org/CodeSystem/v2-0131", 117 | "code": "N" 118 | } 119 | ] 120 | } 121 | ], 122 | "name": { 123 | "_family": { 124 | "extension": [ 125 | { 126 | "url": "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix", 127 | "valueString": "VV" 128 | } 129 | ] 130 | }, 131 | "family": "du Marché", 132 | "given": [ 133 | "Bénédicte" 134 | ] 135 | }, 136 | "telecom": [ 137 | { 138 | "system": "phone", 139 | "value": "+33 (237) 998327" 140 | } 141 | ], 142 | "address": { 143 | "use": "home", 144 | "type": "both", 145 | "line": [ 146 | "534 Erewhon St" 147 | ], 148 | "city": "PleasantVille", 149 | "district": "Rainbow", 150 | "state": "Vic", 151 | "postalCode": "3999", 152 | "period": { 153 | "start": "1974-12-25" 154 | } 155 | }, 156 | "gender": "female", 157 | "period": { 158 | "start": "2012" 159 | } 160 | } 161 | ], 162 | "managingOrganization": { 163 | "reference": "Organization/1" 164 | } 165 | } -------------------------------------------------------------------------------- /tests/resources/quantity-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "QuestionnaireResponse", 3 | "item": [{ 4 | "linkId": "1", 5 | "answer": { 6 | "valueQuantity": { 7 | "value": 2, 8 | "unit": "year(real UCUM code in field 'code')", 9 | "system": "http://unitsofmeasure.org", 10 | "code": "a" 11 | } 12 | } 13 | },{ 14 | "linkId": "2", 15 | "answer": { 16 | "valueQuantity": { 17 | "value": 3, 18 | "unit": "year(real UCUM code in field 'code')", 19 | "system": "http://unitsofmeasure.org", 20 | "code": "min" 21 | } 22 | } 23 | },{ 24 | "linkId": "3", 25 | "answer": { 26 | "valueQuantity": { 27 | "value": 4, 28 | "unit": "year(real UCUM code in field 'code')", 29 | "system": "http://othersystemofmeasure.org", 30 | "code": "min" 31 | } 32 | } 33 | },{ 34 | "linkId": "4", 35 | "answer": { 36 | "valueQuantity": { 37 | "value": 1, 38 | "comparator": ">", 39 | "unit": "year(real UCUM code in field 'code')", 40 | "system": "http://unitsofmeasure.org", 41 | "code": "min" 42 | } 43 | } 44 | }] 45 | } 46 | -------------------------------------------------------------------------------- /tests/resources/questionnaire-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Questionnaire", 3 | "id": "3141", 4 | "text": { 5 | "status": "generated", 6 | "div": "
\n            1.Comorbidity?\n              1.1 Cardial Comorbidity\n                1.1.1 Angina?\n                1.1.2 MI?\n              1.2 Vascular Comorbidity?\n              ...\n            Histopathology\n              Abdominal\n                pT category?\n              ...\n          
" 7 | }, 8 | "url": "http://hl7.org/fhir/Questionnaire/3141", 9 | "title": "Cancer Quality Forum Questionnaire 2012", 10 | "status": "draft", 11 | "subjectType": [ 12 | "Patient" 13 | ], 14 | "date": "2012-01", 15 | "item": [ 16 | { 17 | "linkId": "1", 18 | "code": [ 19 | { 20 | "system": "http://example.org/system/code/sections", 21 | "code": "COMORBIDITY" 22 | } 23 | ], 24 | "type": "group", 25 | "item": [ 26 | { 27 | "linkId": "1.1", 28 | "code": [ 29 | { 30 | "system": "http://example.org/system/code/questions", 31 | "code": "COMORB" 32 | } 33 | ], 34 | "prefix": "1", 35 | "type": "choice", 36 | "answerValueSet": "http://hl7.org/fhir/ValueSet/yesnodontknow", 37 | "item": [ 38 | { 39 | "linkId": "1.1.1", 40 | "code": [ 41 | { 42 | "system": "http://example.org/system/code/sections", 43 | "code": "CARDIAL" 44 | } 45 | ], 46 | "type": "group", 47 | "enableWhen": [ 48 | { 49 | "question": "1.1", 50 | "operator": "=", 51 | "answerCoding": { 52 | "system": "http://terminology.hl7.org/CodeSystem/v2-0136", 53 | "code": "Y" 54 | } 55 | } 56 | ], 57 | "item": [ 58 | { 59 | "linkId": "1.1.1.1", 60 | "code": [ 61 | { 62 | "system": "http://example.org/system/code/questions", 63 | "code": "COMORBCAR" 64 | } 65 | ], 66 | "prefix": "1.1", 67 | "type": "choice", 68 | "answerValueSet": "http://hl7.org/fhir/ValueSet/yesnodontknow", 69 | "item": [ 70 | { 71 | "linkId": "1.1.1.1.1", 72 | "code": [ 73 | { 74 | "system": "http://example.org/system/code/questions", 75 | "code": "COMCAR00", 76 | "display": "Angina Pectoris" 77 | }, 78 | { 79 | "system": "http://snomed.info/sct", 80 | "code": "194828000", 81 | "display": "Angina (disorder)" 82 | } 83 | ], 84 | "prefix": "1.1.1", 85 | "type": "choice", 86 | "answerValueSet": "http://hl7.org/fhir/ValueSet/yesnodontknow" 87 | }, 88 | { 89 | "linkId": "1.1.1.1.2", 90 | "code": [ 91 | { 92 | "system": "http://snomed.info/sct", 93 | "code": "22298006", 94 | "display": "Myocardial infarction (disorder)" 95 | } 96 | ], 97 | "prefix": "1.1.2", 98 | "type": "choice", 99 | "answerValueSet": "http://hl7.org/fhir/ValueSet/yesnodontknow" 100 | } 101 | ] 102 | }, 103 | { 104 | "linkId": "1.1.1.2", 105 | "code": [ 106 | { 107 | "system": "http://example.org/system/code/questions", 108 | "code": "COMORBVAS" 109 | } 110 | ], 111 | "prefix": "1.2", 112 | "type": "choice", 113 | "answerValueSet": "http://hl7.org/fhir/ValueSet/yesnodontknow" 114 | } 115 | ] 116 | } 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | "linkId": "2", 123 | "code": [ 124 | { 125 | "system": "http://example.org/system/code/sections", 126 | "code": "HISTOPATHOLOGY" 127 | } 128 | ], 129 | "type": "group", 130 | "item": [ 131 | { 132 | "linkId": "2.1", 133 | "code": [ 134 | { 135 | "system": "http://example.org/system/code/sections", 136 | "code": "ABDOMINAL" 137 | } 138 | ], 139 | "type": "group", 140 | "item": [ 141 | { 142 | "linkId": "2.1.2", 143 | "code": [ 144 | { 145 | "system": "http://example.org/system/code/questions", 146 | "code": "STADPT", 147 | "display": "pT category" 148 | } 149 | ], 150 | "type": "choice" 151 | } 152 | ] 153 | } 154 | ] 155 | } 156 | ] 157 | } -------------------------------------------------------------------------------- /tests/resources/questionnaire-part-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "answer": [{ 3 | "valueQuantity": { 4 | "value": 2, 5 | "unit": "year(real UCUM code in field 'code')", 6 | "system": "http://unitsofmeasure.org", 7 | "code": "a" 8 | } 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /tests/resources/valueset-example-expansion.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "ValueSet", 3 | "id": "example-expansion", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/StructureDefinition/shareablevalueset" 7 | ] 8 | }, 9 | "text": { 10 | "status": "generated", 11 | "div": "
http://loinc.org14647-2Cholesterol [Moles/volume] in Serum or Plasma
Additional Cholesterol codes
http://loinc.org2093-3Cholesterol [Mass/volume] in Serum or Plasma
http://loinc.org48620-9Cholesterol [Mass/volume] in Serum or Plasma ultracentrifugate
http://loinc.org9342-7Cholesterol [Percentile]
Cholesterol Ratios
http://loinc.org2096-6Cholesterol/Triglyceride [Mass Ratio] in Serum or Plasma
http://loinc.org35200-5Cholesterol/Triglyceride [Mass Ratio] in Serum or Plasma
http://loinc.org48089-7Cholesterol/Apolipoprotein B [Molar ratio] in Serum or Plasma
http://loinc.org55838-7Cholesterol/Phospholipid [Molar ratio] in Serum or Plasma
" 12 | }, 13 | "url": "http://hl7.org/fhir/ValueSet/example-expansion", 14 | "version": "20150622", 15 | "name": "LOINC Codes for Cholesterol in Serum/Plasma", 16 | "status": "draft", 17 | "experimental": true, 18 | "date": "2015-06-22", 19 | "publisher": "FHIR Project team", 20 | "contact": [ 21 | { 22 | "telecom": [ 23 | { 24 | "system": "url", 25 | "value": "http://hl7.org/fhir" 26 | } 27 | ] 28 | } 29 | ], 30 | "description": "This is an example value set that includes all the LOINC codes for serum/plasma cholesterol from v2.36.", 31 | "copyright": "This content from LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at http://loinc.org/terms-of-use.", 32 | "compose": { 33 | "include": [ 34 | { 35 | "system": "http://loinc.org", 36 | "filter": [ 37 | { 38 | "property": "parent", 39 | "op": "=", 40 | "value": "LP43571-6" 41 | } 42 | ] 43 | } 44 | ] 45 | }, 46 | "expansion": { 47 | "extension": [ 48 | { 49 | "url": "http://hl7.org/fhir/StructureDefinition/valueset-expansionSource", 50 | "valueUri": "http://hl7.org/fhir/ValueSet/example-extensional" 51 | } 52 | ], 53 | "identifier": "urn:uuid:42316ff8-2714-4680-9980-f37a6d1a71bc", 54 | "timestamp": "2015-06-22T13:56:07Z", 55 | "total": 8, 56 | "offset": 0, 57 | "parameter": [ 58 | { 59 | "name": "version", 60 | "valueString": "2.50" 61 | } 62 | ], 63 | "contains": [ 64 | { 65 | "system": "http://loinc.org", 66 | "version": "2.50", 67 | "code": "14647-2", 68 | "display": "Cholesterol [Moles/volume] in Serum or Plasma" 69 | }, 70 | { 71 | "abstract": true, 72 | "display": "Cholesterol codes", 73 | "contains": [ 74 | { 75 | "system": "http://loinc.org", 76 | "version": "2.50", 77 | "code": "2093-3", 78 | "display": "Cholesterol [Mass/volume] in Serum or Plasma" 79 | }, 80 | { 81 | "system": "http://loinc.org", 82 | "version": "2.50", 83 | "code": "48620-9", 84 | "display": "Cholesterol [Mass/volume] in Serum or Plasma ultracentrifugate" 85 | }, 86 | { 87 | "system": "http://loinc.org", 88 | "version": "2.50", 89 | "code": "9342-7", 90 | "display": "Cholesterol [Percentile]" 91 | } 92 | ] 93 | }, 94 | { 95 | "abstract": true, 96 | "display": "Cholesterol Ratios", 97 | "contains": [ 98 | { 99 | "system": "http://loinc.org", 100 | "version": "2.50", 101 | "code": "2096-6", 102 | "display": "Cholesterol/Triglyceride [Mass Ratio] in Serum or Plasma" 103 | }, 104 | { 105 | "system": "http://loinc.org", 106 | "version": "2.50", 107 | "code": "35200-5", 108 | "display": "Cholesterol/Triglyceride [Mass Ratio] in Serum or Plasma" 109 | }, 110 | { 111 | "system": "http://loinc.org", 112 | "version": "2.50", 113 | "code": "48089-7", 114 | "display": "Cholesterol/Apolipoprotein B [Molar ratio] in Serum or Plasma" 115 | }, 116 | { 117 | "system": "http://loinc.org", 118 | "version": "2.50", 119 | "code": "55838-7", 120 | "display": "Cholesterol/Phospholipid [Molar ratio] in Serum or Plasma" 121 | } 122 | ] 123 | } 124 | ] 125 | } 126 | } -------------------------------------------------------------------------------- /tests/test_additional.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import pytest 4 | 5 | from fhirpathpy import evaluate 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ("resource", "path"), 10 | [ 11 | ( 12 | { 13 | "resourceType": "Patient", 14 | "name": [{"given": ["First", "Middle"], "family": "Last"}], 15 | }, 16 | "Patient.name.given.toDate()", 17 | ), 18 | ], 19 | ) 20 | def path_functions_test(resource, path): 21 | with pytest.raises(Exception) as e: 22 | evaluate(resource, path) 23 | assert str(e.value) == "to_date called for a collection of length 2" 24 | 25 | 26 | def copy_deepcopy_test(): 27 | copy_1 = copy.copy(evaluate({}, "@2018")) 28 | copy_2 = evaluate({}, "@2018").copy() 29 | 30 | deepcopy_1 = copy.deepcopy(evaluate({}, "@2018")) 31 | 32 | assert copy_1[0] == "2018" 33 | assert copy_2[0] == "2018" 34 | assert deepcopy_1[0] == "2018" 35 | -------------------------------------------------------------------------------- /tests/test_equivalence.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | import pytest 4 | 5 | from fhirpathpy.engine.invocations.equality import ( 6 | decimal_places, 7 | is_equivalent, 8 | normalize_string, 9 | round_to_decimal_places, 10 | ) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ("s", "expected"), 15 | [ 16 | ("ab c", "ab c"), 17 | (" A B C ", "a b c"), 18 | ("dEf", "def"), 19 | (" X y Z ", "x y z"), 20 | ("", ""), 21 | ], 22 | ) 23 | def normalize_string_test(s, expected): 24 | assert normalize_string(s) == expected 25 | 26 | 27 | @pytest.mark.parametrize( 28 | ("a", "expected"), 29 | [ 30 | (1.001, 3), 31 | (1.1, 1), 32 | (1.0, 0), 33 | (0, 0), 34 | (0.00000011, 8), 35 | (0.00000010, 7), 36 | (0.01, 2), 37 | ], 38 | ) 39 | def decimal_places_test(a, expected): 40 | assert decimal_places(a) == expected 41 | 42 | 43 | @pytest.mark.parametrize( 44 | ("a", "n", "expected"), 45 | [ 46 | (1.001, 2, Decimal("1.00")), 47 | (1.1, 1, Decimal("1.1")), 48 | (1.012345, 3, Decimal("1.012")), 49 | (0.123456, 4, Decimal("0.1235")), 50 | (0, 2, Decimal("0")), 51 | (0.00012345, 4, Decimal("0.0001")), 52 | ], 53 | ) 54 | def round_to_decimal_places_test(a, n, expected): 55 | assert round_to_decimal_places(a, n) == expected 56 | 57 | 58 | @pytest.mark.parametrize( 59 | ("a", "b", "expected"), 60 | [ 61 | (1.001, 1.0012, True), 62 | (1.1, 1.101, True), 63 | (1.012345, 1.012346, False), 64 | (0.123456000, 0.123457, False), 65 | (0.123457000, 0.123457, True), 66 | (0.00012345, 0.00012346, False), 67 | (1.0, 1, True), 68 | (0, 0.0001, True), 69 | (1, 0.0001, False), 70 | (0.123, 0.123456, True), 71 | ], 72 | ) 73 | def is_equivalent_test(a, b, expected): 74 | assert is_equivalent(a, b) == expected 75 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | from antlr4.error.Errors import LexerNoViableAltException 6 | 7 | from fhirpathpy.parser import parse 8 | 9 | ast_fixtures_path = Path(__file__).resolve().parent.joinpath("fixtures").joinpath("ast") 10 | 11 | 12 | def load_ast_fixture(fixture_name): 13 | fixture_path = ast_fixtures_path.joinpath(fixture_name + ".json") 14 | with open(fixture_path) as f: 15 | return json.load(f) 16 | 17 | 18 | def are_ast_equal(first_ast, second_ast): 19 | first_string = json.dumps(first_ast, sort_keys=True) 20 | second_string = json.dumps(second_ast, sort_keys=True) 21 | 22 | return first_string == second_string 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "expression", 27 | [ 28 | "4+4", 29 | "object", 30 | "object.method()", 31 | "object.method(42)", 32 | "object.property", 33 | "object.property.method()", 34 | "object.property.method(42)", 35 | ], 36 | ) 37 | def parse_valid_test(expression): 38 | assert parse(expression) != {} 39 | 40 | 41 | def parse_non_valid_test(): 42 | with pytest.raises(LexerNoViableAltException): 43 | parse("!") 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "expression", 48 | [ 49 | "%v+2", 50 | "a.b+2", 51 | "Observation.value", 52 | "Patient.name.given", 53 | ], 54 | ) 55 | def output_correct_ast_test(expression): 56 | assert are_ast_equal(parse(expression), load_ast_fixture(expression)) 57 | -------------------------------------------------------------------------------- /tests/test_real.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | from fhirpathpy import compile, evaluate 4 | 5 | 6 | def find_concept_test(): 7 | env = {} 8 | env["Source"] = { 9 | "resourceType": "Bundle", 10 | "id": -1, 11 | "entry": [ 12 | { 13 | "resource": { 14 | "resourcceType": "ValueSet", 15 | "expansion": { 16 | "contains": [ 17 | { 18 | "system": "http://snomed.info/sct", 19 | "code": "16291000122106", 20 | "display": "Ifenprodil", 21 | }, 22 | { 23 | "system": "http://snomed.info/sct", 24 | "code": "789067004", 25 | "display": "Amlodipine benzoate", 26 | }, 27 | { 28 | "system": "http://snomed.info/sct", 29 | "code": "783132009", 30 | "display": "Bosentan hydrochloride", 31 | }, 32 | { 33 | "system": "http://snomed.info/sct", 34 | "code": "766924002", 35 | "display": "Substance with nicotinic receptor antagonist mechanism of action", 36 | }, 37 | { 38 | "system": "http://snomed.info/sct", 39 | "code": "708177006", 40 | "display": "Betahistine mesilate (substance)", 41 | }, 42 | ] 43 | }, 44 | } 45 | } 46 | ], 47 | } 48 | 49 | env["Coding"] = { 50 | "system": "http://snomed.info/sct", 51 | "code": "708177006", 52 | "display": "Betahistine mesilate (substance)", 53 | } 54 | 55 | assert evaluate( 56 | {}, 57 | "%Source.entry[0].resource.expansion.contains.where(code=%Coding.code)!~{}", 58 | env, 59 | ) == [True] 60 | 61 | env["Coding"] = ( 62 | { 63 | "system": "http://snomed.info/sct", 64 | "code": "428159003", 65 | "display": "Ambrisentan", 66 | }, 67 | ) 68 | 69 | assert evaluate( 70 | {}, 71 | "%Source.entry[0].resource.expansion.contains.where(code=%Coding.code)!~{}", 72 | env, 73 | ) == [False] 74 | 75 | 76 | def aidbox_polimorphici_test(): 77 | qr = { 78 | "resourceType": "QuestionnaireResponse", 79 | "item": {"linkId": "foo", "answer": {"value": {"Coding": {"code": 1}}}}, 80 | } 81 | assert evaluate(qr, "QuestionnaireResponse.item.answer.value.Coding") == [{"code": 1}] 82 | 83 | 84 | def pickle_test(): 85 | resource = { 86 | "resourceType": "DiagnosticReport", 87 | "id": "abc", 88 | "subject": {"reference": "Patient/cdf"}, 89 | } 90 | path = compile(path="DiagnosticReport.subject.reference") 91 | 92 | dumped = pickle.dumps(path) 93 | reload = pickle.loads(dumped) 94 | 95 | assert reload(resource) == ["Patient/cdf"] 96 | 97 | 98 | def extension_test(): 99 | patient = { 100 | "identifier": [ 101 | { 102 | "period": {"start": "2020-01-01"}, 103 | "system": "http://hl7.org/fhir/sid/us-mbi", 104 | "type": { 105 | "coding": [ 106 | { 107 | "code": "MC", 108 | "display": "Patient's Medicare number", 109 | "extension": [ 110 | { 111 | "url": "https://bluebutton.cms.gov/resources/codesystem/identifier-currency", 112 | "valueCoding": { 113 | "code": "current", 114 | "display": "Current", 115 | "system": "https://bluebutton.cms.gov/resources/codesystem/identifier-currency", 116 | }, 117 | } 118 | ], 119 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203", 120 | } 121 | ] 122 | }, 123 | "value": "7SM0A00AA00", 124 | } 125 | ], 126 | "resourceType": "Patient", 127 | } 128 | result = evaluate( 129 | patient, 130 | "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", 131 | ) 132 | assert result == ["7SM0A00AA00"] 133 | 134 | 135 | def reference_filter_test(): 136 | encounter = { 137 | "meta": { 138 | "lastUpdated": "2023-08-03T16:24:50.634670Z", 139 | "versionId": "256452", 140 | "extension": [{"url": "ex:createdAt", "valueInstant": "2023-07-31T18:06:57.172516Z"}], 141 | }, 142 | "type": [ 143 | { 144 | "coding": [ 145 | { 146 | "code": "primary", 147 | "system": "http://prenosis.com/fhir/CodeSystem/encounter-type", 148 | } 149 | ] 150 | } 151 | ], 152 | "participant": [ 153 | {"individual": {"reference": "Practitioner/dr-johns"}}, 154 | {"individual": {"reference": "PractitionerRole/dr-brown-internal"}}, 155 | ], 156 | "resourceType": "Encounter", 157 | "status": "finished", 158 | "id": "25c724fe-8664-4620-be95-ebf8b4784339", 159 | "class": { 160 | "code": "IMP", 161 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", 162 | "display": "inpatient encounter", 163 | }, 164 | "identifier": [ 165 | { 166 | "value": "00c4ba23-85a4-4a30-89d8-d907814028b6", 167 | "system": "http://prenosis.com/fhir/CodeSystem/encounter-identifier", 168 | } 169 | ], 170 | "period": {"end": "2023-07-01T18:53:48.034300Z", "start": "2023-07-01T18:06:56.034300Z"}, 171 | "subject": {"reference": "Patient/a3e9f617-fb12-447c-b931-96f60e0d7dab"}, 172 | } 173 | result = evaluate( 174 | encounter, 175 | "Encounter.participant.individual.reference.where($this.matches('Practitioner/'))", 176 | ) 177 | assert result == ["Practitioner/dr-johns"] 178 | 179 | 180 | def compile_with_user_defined_table_test(): 181 | user_invocation_table = { 182 | "pow": { 183 | "fn": lambda inputs, exp=2: [i**exp for i in inputs], 184 | "arity": {0: [], 1: ["Integer"]}, 185 | } 186 | } 187 | 188 | expr = compile( 189 | "a.pow()", 190 | options={"userInvocationTable": user_invocation_table}, 191 | ) 192 | assert expr({"a": [5, 6, 7]}) == [5 * 5, 6 * 6, 7 * 7] 193 | --------------------------------------------------------------------------------