Skip to content

Commit

Permalink
Merge pull request #45 in LFOR/fhirpath.js from feature/LF-2386/conve…
Browse files Browse the repository at this point in the history
…rt-internal-types-to-strings to master

* commit '7b894266d6e86ad0ff81f8652f83bfd73e61fd13':
  Fixed issues found during review
  Fixed issues found during review
  Fixed issues found during review
  Convert instances of internal data types in a result of FHIRPath expression
  • Loading branch information
yuriy-sedinkin committed Aug 31, 2022
2 parents 4fd0d78 + 7b89426 commit efe1919
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 38 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [3.0.0] - 2022-08-25
### Added
- Option `resolveInternalTypes` to control whether any instances of internal data
types (e.g. FP_DateTime, FP_Time, FP_Quantity) in a result of FHIRPath
expression should be converted to standard JavaScript types.
- Method `resolveInternalTypes` which converts any instances of internal data
types (e.g. FP_DateTime, FP_Time, FP_Quantity) in a result of FHIRPath
expression evaluation to standard JavaScript types.
### Changed
- By default, any instances of internal data types (e.g. FP_DateTime, FP_Time,
FP_Quantity) in a result of FHIRPath expression are converted to strings.

## [2.14.7] - 2022-08-15
### Fixed
- Fixed directly (without member invocation) accessing the value of a variable in the context if this value was fetched
Expand Down
91 changes: 71 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,47 +40,98 @@ handling is added, so they are kept seperate from the main FHIRPath file.)
These will define additional global variables like "fhirpath_dstu2_model",
"fhirpath_stu3_model" or "fhirpath_r4_model".

## Usage
## API Usage

Evaluating FHIRPath:

```js
evaluate(resourceObject, fhirPathExpression, environment, model, options);
```
// Evaluating FHIRPath
// API: evaluate(resourceObject, fhirPathExpression, environment)
// Note: The resource will be modified by this function to add type information.

Note: The resource will be modified by this function to add type information.

Basic example:

```js
fhirpath.evaluate({"resourceType": "Patient", ...}, 'Patient.name.given');
```

Environment variables can be passed in as third argument as a hash of name/value
pairs:

// Environment variables can be passed in as third argument as a hash of
// name/value pairs:
```js
fhirpath.evaluate({}, '%a - 1', {a: 5});
```

To include FHIR model data (for support of choice types), pass in the model data
object as the fourth argument:

// To include FHIR model data (for support of choice types), pass in the model
// data object as the fourth argument:
```js
fhirpath.evaluate({"resourceType": "Observation", "valueString": "green"},
'Observation.value', null, fhirpath_r4_model);
```

// If the first parameter is a part of a resource, the second parameter should
// be an object with properties "base" and "expression":
// base - the path in the resource that represents the partial resource being
// used as the context,
// expression - fhirpath expression relative to base.
If the first parameter is a part of a resource, the second parameter should be
an object with properties "base" and "expression":
base - the path in the resource that represents the partial resource being used
as the context,
expression - fhirpath expression relative to base.

```js
fhirpath.evaluate({ "answer": { "valueQuantity": ...}},
{ "base": "QuestionnaireResponse.item",
"expression": "answer.value = 2 year"},
null, fhirpath_r4_model);
```

Precompiling fhirpath - result can be reused against multiple resources:

// Precompiling fhirpath - result can be reused against multiple resources
```js
const path = fhirpath.compile('Patient.name.given', fhirpath_r4_model);
var res = path({"resourceType": "Patient", ...}, {a: 5, ...});
```

If you are going to use the above "precompile" option with a part of a resource,
the first parameter should be an object with properties "base" and "expression":
base - the path in the resource that represents the partial resource being used
as the context,
expression - fhirpath expression relative to base.

// If you are going to use the above "precompile" option with a part of a resource,
// the first parameter should be an object with properties "base" and "expression":
// base - the path in the resource that represents the partial resource being
// used as the context,
// expression - fhirpath expression relative to base.
```js
const path = fhirpath.compile({ "base": "QuestionnaireResponse.item",
"expression": "answer.value = 2 year"},
fhirpath_r4_model);
var res = path({ "answer": { "valueQuantity": ...}, {a: 5, ...});
```
During expression evaluation, some values or parts of values may have internal
data types (e.g. FP_DateTime, FP_Time, FP_Quantity). By default, all of these
values are converted to standard JavaScript types, but if you need to use the
result of evaluation as a context variable for another FHIRpath expression,
it would be best to preserve the internal data types. To do this you can use
the option "resolveInternalTypes" = false:
```js
const contextVariable = fhirpath.evaluate(
resource, expression, context, model, {resolveInternalTypes: false}
);
```
This option may also be passed to compile function:
```js
const path = fhirpath.compile(
expression, model, {resolveInternalTypes: false}
);
```
If at some point you decide to convert all values which have internal types to
standard JavaScript types you can use the special function "resolveInternalTypes":
```js
const res = fhirpath.resolveInternalTypes(value);
```
## fhirpath CLI
Expand Down Expand Up @@ -210,7 +261,7 @@ the root of the project directory, and then run "npm run generateParser".
### Building the demo page
```
```sh
npm install && npm run build
cd demo
npm install && npm run build && npm run start
Expand Down
14 changes: 12 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
declare module "fhirpath" {
export function compile(path: string | Path, model?: Model): Compile;
export function compile(
path: string | Path,
model?: Model,
options?: {
resolveInternalTypes?: boolean
}
): Compile;
export function evaluate(
fhirData: any,
path: string | Path,
context: Context,
model?: Model
model?: Model,
options?: {
resolveInternalTypes?: boolean
}
): any[];
export function resolveInternalTypes(value: any): any;
}

declare module "fhirpath/fhir-context/dstu2" {
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fhirpath",
"version": "2.14.7",
"version": "3.0.0",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"dependencies": {
Expand Down
61 changes: 52 additions & 9 deletions src/fhirpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -627,8 +627,11 @@ function parse(path) {
* @param {object} context - a hash of variable name/value pairs.
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
* For example, you could pass in the result of require("fhirpath/fhir-context/r4");
* @param {object} [options] - additional options:
* @param {boolean} [options.resolveInternalTypes] - whether values of internal
* types should be converted to strings, true by default.
*/
function applyParsedPath(resource, parsedPath, context, model) {
function applyParsedPath(resource, parsedPath, context, model, options) {
constants.reset();
let dataRoot = util.arraify(resource);
// doEval takes a "ctx" object, and we store things in that as we parse, so we
Expand Down Expand Up @@ -660,15 +663,21 @@ function applyParsedPath(resource, parsedPath, context, model) {
// Path for the data extracted from the resource.
let path = firstRtn instanceof ResourceNode ? firstRtn.path : null;

// Resolve any internal "ResourceNode" instances. Continue to let FP_Type
// subclasses through.
// Resolve any internal "ResourceNode" instances to plain objects and if
// options.resolveInternalTypes is true, resolve any internal "FP_Type"
// instances to strings.
rtn = (function visit(n) {
n = util.valData(n);
if (Array.isArray(n)) {
for (let i=0, len=n.length; i<len; ++i)
n[i] = visit(n[i]);
}
else if (typeof n === 'object' && !(n instanceof FP_Type)) {
else if (n instanceof FP_Type) {
if (options.resolveInternalTypes) {
n = n.toString();
}
}
else if (typeof n === 'object') {
for (let k of Object.keys(n))
n[k] = visit(n[k]);
}
Expand All @@ -682,6 +691,27 @@ function applyParsedPath(resource, parsedPath, context, model) {
return rtn;
}

/**
* Resolves any internal "FP_Type" instances in a result of FHIRPath expression
* evaluation to standard JavaScript types.
* @param {any} val - a result of FHIRPath expression evaluation
* @returns {any} a new object with resolved values.
*/
function resolveInternalTypes(val) {
if (Array.isArray(val)) {
for (let i=0, len=val.length; i<len; ++i)
val[i] = resolveInternalTypes(val[i]);
}
else if (val instanceof FP_Type) {
val = val.toString();
}
else if (typeof val === 'object') {
for (let k of Object.keys(val))
val[k] = resolveInternalTypes(val[k]);
}
return val;
}

/**
* Evaluates the "path" FHIRPath expression on the given resource or part of the resource,
* using data from "context" for variables mentioned in the "path" expression.
Expand All @@ -695,9 +725,14 @@ function applyParsedPath(resource, parsedPath, context, model) {
* @param {object} context - a hash of variable name/value pairs.
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
* For example, you could pass in the result of require("fhirpath/fhir-context/r4");
* @param {object} [options] - additional options:
* @param {boolean} [options.resolveInternalTypes] - whether values of internal
* types should be converted to standard JavaScript types (true by default).
* If false is passed, this conversion can be done later by calling
* resolveInternalTypes().
*/
function evaluate(fhirData, path, context, model) {
return compile(path, model)(fhirData, context);
function evaluate(fhirData, path, context, model, options) {
return compile(path, model, options)(fhirData, context);
}

/**
Expand All @@ -712,21 +747,28 @@ function evaluate(fhirData, path, context, model) {
* @param {string} path.expression - FHIRPath expression relative to path.base
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
* For example, you could pass in the result of require("fhirpath/fhir-context/r4");
* @param {object} [options] - additional options:
* @param {boolean} [options.resolveInternalTypes] - whether values of internal
* types should be converted to strings, true by default.
*/
function compile(path, model) {
function compile(path, model, options) {
options = {
resolveInternalTypes: true,
... options
};
if (typeof path === 'object') {
const node = parse(path.expression);
return function (fhirData, context) {
const inObjPath = fhirData && fhirData.__path__;
const resource = makeResNode(fhirData, path.base || inObjPath);
return applyParsedPath(resource, node, context, model);
return applyParsedPath(resource, node, context, model, options);
};
} else {
const node = parse(path);
return function (fhirData, context) {
const inObjPath = fhirData && fhirData.__path__;
const resource = inObjPath ? makeResNode(fhirData, inObjPath) : fhirData;
return applyParsedPath(resource, node, context, model);
return applyParsedPath(resource, node, context, model, options);
};
}
}
Expand All @@ -736,6 +778,7 @@ module.exports = {
parse,
compile,
evaluate,
resolveInternalTypes,
// Might as well export the UCUM library, since we are using it.
ucumUtils: require('@lhncbc/ucum-lhc').UcumLhcUtils.getInstance()
};
72 changes: 71 additions & 1 deletion test/api.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fhirpath = require('../src/fhirpath');
const r4_model = require('../fhir-context/r4');
const _ = require('lodash');
const {FP_DateTime, FP_Quantity} = require('../src/types');

describe('compile', () => {
it('should accept a model object', () => {
Expand Down Expand Up @@ -37,6 +38,30 @@ describe('compile', () => {
let result = execExpression({}, {partOfResource});
expect(result).toStrictEqual([true]);
});

it('should resolve values which have internal data types to strings by default', () => {
let f = fhirpath.compile('@2018-02-18T12:23:45-05:00');
expect(f({})).toStrictEqual(['2018-02-18T12:23:45-05:00']);

f = fhirpath.compile("2.0 'cm'");
expect(f({})).toStrictEqual(["2 'cm'"]);
});

it('should not resolve values which have internal data types to strings when options.resolveInternalTypes is false', () => {
let f = fhirpath.compile(
'@2018-02-18T12:23:45-05:00',
null,
{resolveInternalTypes: false}
);
expect(f({})).toStrictEqual([new FP_DateTime('2018-02-18T12:23:45-05:00')]);

f = fhirpath.compile(
"2.0 'cm'",
null,
{resolveInternalTypes: false}
);
expect(f({})).toStrictEqual([new FP_Quantity(2, "'cm'")]);
});
});

describe('evaluate', () => {
Expand Down Expand Up @@ -84,5 +109,50 @@ describe('evaluate', () => {
);
expect(result).toEqual(['1', '2', '3', '4']);
expect(someVar).toStrictEqual(someVarOrig);
})
});

it('should resolve values which have internal data types to strings by default', () => {
expect(
fhirpath.evaluate({}, '@2018-02-18T12:23:45-05:00')
).toStrictEqual(['2018-02-18T12:23:45-05:00']);

expect(
fhirpath.evaluate({}, "2.0 'cm'")
).toStrictEqual(["2 'cm'"]);
});

it('should not resolve values which have internal data types to strings when options.resolveInternalTypes is false', () => {
expect(
fhirpath.evaluate(
{},
'@2018-02-18T12:23:45-05:00',
null,
null,
{ resolveInternalTypes: false })
).toStrictEqual([new FP_DateTime('2018-02-18T12:23:45-05:00')]);

expect(
fhirpath.evaluate(
{},
"2.0 'cm'",
null,
null,
{ resolveInternalTypes: false }
)
).toStrictEqual([new FP_Quantity(2, "'cm'")]);
});
});

describe('resolveInternalTypes', () => {
it('should resolve values which have internal data types to strings', () => {
expect(
fhirpath.resolveInternalTypes([
new FP_DateTime('2020-02-18T12:23:45-05:00'),
new FP_Quantity(1, "'cm'")
])
).toStrictEqual([
'2020-02-18T12:23:45-05:00',
"1 'cm'"
]);
});
});
Loading

0 comments on commit efe1919

Please sign in to comment.