Skip to content

Commit

Permalink
Merge pull request #53 in LFOR/fhirpath.js from feature/updates-from-…
Browse files Browse the repository at this point in the history
…brian to master

* commit '35792e9fc569dc47da648c6f0a1affb76e9fe71e': (37 commits)
  Updated version
  whitespace correction and changelog updated
  lint and test message cleanups
  Update the optionality of model/context parameters
  remove the toLowerCase() as requested (spec isn't specific here, so don't force it)
  Extra test as requested that has an `+` in the output
  Usage notes for the trace function in the readme
  Split function should not remove empty strings. If this FHIR issue resolves the other way, we will need to revert this part.
  Trace method incorrect documentation - resolved Also updated the typescript definition to require the model as it is not optional.
  Throw an error if the input collection isn't all strings.
  As per the updated spec, the join parameter is optional. https://build.fhir.org/ig/HL7/FHIRPath/#joinseparator-string-string
  throw an error if there are an uneven number of characters to decode
  Minor merge issue
  Missed including the min.r5.js file from the build step
  Unit test the R5 Observation model and various other updates to support building the content also.
  #137 Add support for FHIR R5
  #121 Update the unit tests and include the ones from the FHIRPath spec tests
  #121 Implement the `urlbase64` format encoding correctly (this isn't the UriEncoding)
  #121 The `trim` function is only defined on a single string, so don't process the input as though it was a collection.
  #121 Update the `hex` encoding/decode to comply with the spec
  ...
  • Loading branch information
yuriy-sedinkin committed May 3, 2023
2 parents 2ce3e8d + 35792e9 commit bfc097e
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ test/benchmark/results
test/cypress/screenshots
test/cypress/videos
test/cypress/downloads
.vs/
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [3.5.0] - 2023-05-04
### Added
- Add `split` and `join` functions
- Add `encode` and `decode` functions
- Added a callback (traceFn) to the options object for the `trace` function
### Fixed
- Update the typescript definition to mark context and model as optional
- Corect the `trace` function's name parameter is required

## [3.4.0] - 2023-04-26
### Added
- support for FHIR R5 publication.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ const res = fhirpath.types(
);
```
If you want to capture evaluations of the `trace` method, you can include that in the options object.
```js
let tracefunction = function (x, label) {
console.log("Trace output [" + label + "]: ", x);
};

const res = fhirpath.evaluate(contextNode, path, environment, fhirpath_r4_model, { traceFn: tracefunction });
```
## fhirpath CLI
Expand Down
7 changes: 4 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ declare module "fhirpath" {
export function evaluate(
fhirData: any,
path: string | Path,
context: Context,
context?: Context,
model?: Model,
options?: {
resolveInternalTypes?: boolean
resolveInternalTypes?: boolean,
traceFn?: (value: any, label: string) => void
}
): any[];
export function resolveInternalTypes(value: any): any;
Expand Down Expand Up @@ -75,6 +76,6 @@ interface Model {
};
}

type Compile = (resource: any, context: Context) => any[];
type Compile = (resource: any, context?: Context) => any[];

type Context = void | Record<string, any>;
4 changes: 2 additions & 2 deletions 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": "3.4.0",
"version": "3.5.0",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"dependencies": {
Expand Down
24 changes: 18 additions & 6 deletions src/fhirpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ engine.invocationTable = {
union: {fn: combining.union, arity: {1: ["AnyAtRoot"]}},
intersect: {fn: combining.intersect, arity: {1: ["AnyAtRoot"]}},
iif: {fn: misc.iifMacro, arity: {2: ["Expr", "Expr"], 3: ["Expr", "Expr", "Expr"]}},
trace: {fn: misc.traceFn, arity: {0: [], 1: ["String"]}},
trace: {fn: misc.traceFn, arity: {1: ["String"], 2: ["String", "Expr"]}},
toInteger: {fn: misc.toInteger},
toDecimal: {fn: misc.toDecimal},
toString: {fn: misc.toString},
Expand Down Expand Up @@ -127,6 +127,12 @@ engine.invocationTable = {
replaceMatches: {fn: strings.replaceMatches, arity: {2: ["String", "String"]}},
length: {fn: strings.length },
toChars: {fn: strings.toChars },
join: {fn: strings.joinFn, arity: {0: [], 1: ["String"]}},
split: {fn: strings.splitFn, arity: {1: ["String"]}},
trim: {fn: strings.trimFn},

encode: {fn: strings.encodeFn, arity: {1: ["String"]}},
decode: {fn: strings.decodeFn, arity: {1: ["String"]}},

abs: {fn: math.abs},
ceiling: {fn: math.ceiling},
Expand Down Expand Up @@ -629,11 +635,12 @@ function parse(path) {
* @param {(object|object[])} resource - FHIR resource, bundle as js object or array of resources
* This resource will be modified by this function to add type information.
* @param {object} parsedPath - a special object created by the parser that describes the structure of a fhirpath expression.
* @param {object} context - a hash of variable name/value pairs.
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
* @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
* @param {function} [options.traceFn] - An optional trace function to call when tracing
* types should be converted to strings, true by default.
*/
function applyParsedPath(resource, parsedPath, context, model, options) {
Expand All @@ -658,6 +665,9 @@ function applyParsedPath(resource, parsedPath, context, model, options) {
}, {});
}
let ctx = {dataRoot, vars: Object.assign(vars, context), model};
if (options && options.traceFn) {
ctx.customTraceFn = options.traceFn;
}
return engine.doEval(ctx, dataRoot, parsedPath.children[0])
// engine.doEval returns array of "ResourceNode" and/or "FP_Type" instances.
// "ResourceNode" or "FP_Type" instances are not created for sub-items.
Expand Down Expand Up @@ -713,11 +723,12 @@ function resolveInternalTypes(val) {
* or object, if fhirData represents the part of the FHIR resource:
* @param {string} path.base - base path in resource from which fhirData was extracted
* @param {string} path.expression - FHIRPath expression relative to path.base
* @param {object} context - a hash of variable name/value pairs.
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
* @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
* @param {function} [options.traceFn] - An optional trace function to call when tracing
* types should be converted to standard JavaScript types (true by default).
* If false is passed, this conversion can be done later by calling
* resolveInternalTypes().
Expand All @@ -736,10 +747,11 @@ function evaluate(fhirData, path, context, model, options) {
* @param {string} path.base - base path in resource from which a part of
* the resource was extracted
* @param {string} path.expression - FHIRPath expression relative to path.base
* @param {object} model - The "model" data object specific to a domain, e.g. R4.
* @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
* @param {function} [options.traceFn] - An optional trace function to call when tracing
* types should be converted to strings, true by default.
*/
function compile(path, model, options) {
Expand Down
19 changes: 17 additions & 2 deletions src/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,23 @@ engine.iifMacro = function(data, cond, ok, fail) {
}
};

engine.traceFn = function(x, label) {
console.log("TRACE:[" + (label || "") + "]", JSON.stringify(x, null, " "));
engine.traceFn = function (x, label, expr) {
if (this.customTraceFn) {
if (expr){
this.customTraceFn(expr(x), label ?? "");
}
else {
this.customTraceFn(x, label ?? "");
}
}
else {
if (expr){
console.log("TRACE:[" + (label || "") + "]", JSON.stringify(expr(x), null, " "));
}
else {
console.log("TRACE:[" + (label || "") + "]", JSON.stringify(x, null, " "));
}
}
return x;
};

Expand Down
16 changes: 15 additions & 1 deletion src/polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,18 @@ if (!Object.assign) {
}, Object(target));
}
});
}
}

// Define btoa for NodeJS
if (typeof btoa === 'undefined') {
global.btoa = function (str) {
return new Buffer.from(str, 'binary').toString('base64');
};
}

// Define atob for NodeJS
if (typeof atob === 'undefined') {
global.atob = function (b64Encoded) {
return new Buffer.from(b64Encoded, 'base64').toString('binary');
};
}
96 changes: 80 additions & 16 deletions src/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function rewritePatternForDotAll(pattern) {
// The last index of unescaped ']'
const lastIndexOfCloseBracket = cleanPrecedingPart.lastIndexOf(']');
return escaped ||
(lastIndexOfOpenBracket > lastIndexOfCloseBracket)
(lastIndexOfOpenBracket > lastIndexOfCloseBracket)
? '.'
: '[^]';
});
Expand All @@ -40,54 +40,118 @@ function rewritePatternForDotAll(pattern) {
return cachedRegExp[pattern];
}

engine.indexOf = function(coll, substr){
engine.indexOf = function (coll, substr) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(substr) || util.isEmpty(str) ? [] : str.indexOf(substr);
};

engine.substring = function(coll, start, length){
engine.substring = function (coll, start, length) {
const str = misc.singleton(coll, 'String');
if (util.isEmpty(str) || util.isEmpty(start) || start < 0 || start >= str.length) {
return [];
return [];
}
if (length === undefined || util.isEmpty(length)) {
return str.substring(start);
}
return str.substring(start, start + length);
};

engine.startsWith = function(coll, prefix){
engine.startsWith = function (coll, prefix) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(prefix) || util.isEmpty(str) ? [] : str.startsWith(prefix);
};

engine.endsWith = function(coll, postfix) {
engine.endsWith = function (coll, postfix) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(postfix) || util.isEmpty(str) ? [] : str.endsWith(postfix);
};

engine.containsFn = function(coll, substr){
engine.containsFn = function (coll, substr) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(substr) || util.isEmpty(str) ? [] : str.includes(substr);
};

engine.upper = function(coll){
engine.upper = function (coll) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(str) ? [] : str.toUpperCase();
};


engine.lower = function(coll){
engine.lower = function (coll) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(str) ? [] : str.toLowerCase();
};

engine.joinFn = function (coll, separator) {
const stringValues = coll.map((n) => {
const d = util.valData(n);
if (typeof d === "string") {
return d;
}
throw new Error('Join requires a collection of strings.');
});
if (separator === undefined) {
separator = "";
}
return stringValues.join(separator);
};

engine.splitFn = function (coll, separator) {
const strToSplit = misc.singleton(coll, 'String');
return util.isEmpty(strToSplit) ? [] : strToSplit.split(separator);
};

engine.trimFn = function (coll) {
const strToTrim = misc.singleton(coll, 'String');
return util.isEmpty(strToTrim) ? [] : strToTrim.trim();
};

// encoding/decoding
engine.encodeFn = function (coll, format) {
const strToEncode = misc.singleton(coll, 'String');
if (util.isEmpty(strToEncode)){
return [];
}
if (format === 'urlbase64' || format === 'base64url'){
return btoa(strToEncode).replace(/\+/g, '-').replace(/\//g, '_');
}
if (format === 'base64'){
return btoa(strToEncode);
}
if (format === 'hex'){
return Array.from(strToEncode).map(c =>
c.charCodeAt(0) < 128 ? c.charCodeAt(0).toString(16) :
encodeURIComponent(c).replace(/%/g,'')
).join('');
}
return [];
};

engine.decodeFn = function (coll, format) {
const strDecode = misc.singleton(coll, 'String');
if (util.isEmpty(strDecode)){
return [];
}
if (format === 'urlbase64' || format === 'base64url'){
return atob(strDecode.replace(/-/g, '+').replace(/_/g, '/'));
}
if (format === 'base64'){
return atob(strDecode);
}
if (format === 'hex'){
if (strDecode.length % 2 !== 0){
throw new Error('Decode \'hex\' requires an even number of characters.');
}
return decodeURIComponent('%' + strDecode.match(/.{2}/g).join('%'));
}
return [];
};

// Check if dotAll is supported.
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll for details.
const dotAllIsSupported = (new RegExp('')).dotAll === false;

if (dotAllIsSupported) {
engine.matches = function(coll, regex) {
engine.matches = function (coll, regex) {
const str = misc.singleton(coll, 'String');
if (util.isEmpty(regex) || util.isEmpty(str)) {
return [];
Expand All @@ -96,7 +160,7 @@ if (dotAllIsSupported) {
return reg.test(str);
};
} else {
engine.matches = function(coll, regex) {
engine.matches = function (coll, regex) {
const str = misc.singleton(coll, 'String');
if (util.isEmpty(regex) || util.isEmpty(str)) {
return [];
Expand All @@ -106,7 +170,7 @@ if (dotAllIsSupported) {
};
}

engine.replace = function(coll, pattern, repl){
engine.replace = function (coll, pattern, repl) {
const str = misc.singleton(coll, 'String');
if (util.isEmpty(pattern) || util.isEmpty(repl) || util.isEmpty(str)) {
return [];
Expand All @@ -115,7 +179,7 @@ engine.replace = function(coll, pattern, repl){
return str.replace(reg, repl);
};

engine.replaceMatches = function(coll, regex, repl){
engine.replaceMatches = function (coll, regex, repl) {
const str = misc.singleton(coll, 'String');
if (util.isEmpty(regex) || util.isEmpty(repl) || util.isEmpty(str)) {
return [];
Expand All @@ -124,12 +188,12 @@ engine.replaceMatches = function(coll, regex, repl){
return str.replace(reg, repl);
};

engine.length = function(coll){
engine.length = function (coll) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(str) ? [] : str.length;
};

engine.toChars = function(coll){
engine.toChars = function (coll) {
const str = misc.singleton(coll, 'String');
return util.isEmpty(str) ? [] : str.split('');
};
Expand Down
Loading

0 comments on commit bfc097e

Please sign in to comment.