diff --git a/CHANGELOG.md b/CHANGELOG.md index 047ab68..3b796c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ This log documents significant changes for each release. This project follows [Semantic Versioning](http://semver.org/). +## [2.6.0] - 2020-09-01 +### Added +Limited support for types (see README.md for details): +- Function is(type) and operator "is" +- Function ofType(type) + ## [2.5.0] - 2020-08-26 ### Added - Function union(other: collection) diff --git a/README.md b/README.md index afe9b3a..b6ce6ab 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,9 @@ some behavior may still be following the previous version, STU1. The core parser was generated from the FHIRPath ANTLR grammar. Completed sections: -- 3 (Path selection) - except that "is" and "as" are not supported yet +- 3 (Path selection) - 5.1 (Existence) -- 5.2 (Filtering and Projection) "ofType" - basic support for primitives +- 5.2 (Filtering and Projection) "ofType" - limited support for types (see below) - 5.3 (Subsetting) - 5.4 (Combining) - 5.6 (String Manipulation) @@ -149,13 +149,23 @@ Almost completed sections: We are deferring handling information about FHIR resources, as much as possible, with the exception of support for choice types. This affects implementation of the following sections: -- 6.3 (Types) - deferred +- 6.3 (Types) - "is" - limited support for types(see below), + "as" is not supported yet Also, because in JSON DateTime and Time types are represented as strings, if a string in a resource looks like a DateTime or Time (matches the regular expression defined for those types in FHIR), the string will be interpreted as a DateTime or Time. +### Limited support for types: +Currently, the type of the resource property value is used to determine the type, +without using the FHIR specification. This shortcut causes the following issues: +- Type hierarchy is not supported; +- FHIR.uri, FHIR.code, FHIR.oid, FHIR.id, FHIR.uuid, FHIR.sid, FHIR.markdown, FHIR.base64Binary are treated as FHIR.string; +- FHIR.unsignedInt, FHIR.positiveInt are treated as FHIR.integer; +- Also, a property could be specified as FHIR.decimal, but treated as FHIR.integer; +- For date-time related types, only FHIR.dateTime, FHIR.time, System.DateTime and System.Time are supported. + ## Development Notes This section is for people doing development on this package (as opposed to diff --git a/converter/bin/index.js b/converter/bin/index.js index 18e109e..fd34715 100755 --- a/converter/bin/index.js +++ b/converter/bin/index.js @@ -5,6 +5,8 @@ const sourceDir = __dirname + '/../dataset/'; // Directory for converter output(for YAML and JSON files) const destDir = __dirname + '/../../test/'; +// FHIR model used for test cases +const model = 'r4'; // Descriptions for file generation: // [, , ] const sources = [ @@ -64,7 +66,7 @@ commander for (let i = 0; i < testFiles.length; i++) { const [xmlFilename, yamlFilename] = testFiles[i]; - await convert.testsXmlFileToYamlFile(sourceDir + xmlFilename, destDir + yamlFilename); + await convert.testsXmlFileToYamlFile(sourceDir + xmlFilename, destDir + yamlFilename, model); } } catch(e) { console.error(e); diff --git a/converter/converter.js b/converter/converter.js index 78227c6..fb84a73 100644 --- a/converter/converter.js +++ b/converter/converter.js @@ -57,28 +57,35 @@ const castValue = (value, type) => mapper[type](value); /** * Converts Object representing test cases from XML to Object that can be serialized to YAML * @param {Object} node - result of xml2js.parseString + * @param {string} model - model name, e.g. 'r4','stu3', 'dstu2' * @return {Object} */ -const transform = (node) => { +const transform = (node, model = null) => { return Object.keys(node).reduce((acc, key) => { switch(key) { case 'tests': - return { tests: transform(_.pick(node[key], 'group')) }; + return { tests: transform(_.pick(node[key], 'group'), model) }; case 'group': return [...acc, ...node[key].map(item => - ({ [`group: ${item['$'].description || item['$'].name}`]: transform(_.pick(item, 'test')) }))]; + ({ [`group: ${item['$'].description || item['$'].name}`]: transform(_.pick(item, 'test'), model) }))]; case 'test': return [...acc, ...node[key].map(item => { - let test = transform(item); + let test = transform(item, model); if (!test.hasOwnProperty('result') && !test.error) { test.result = []; } if (!validateTest(test)) { - test.disable = true; + if (model && validateTest(Object.assign({}, test, { model }))) { + // if the test cannot be passed without set the model, we set the model + test.model = model; + } else { + // if the test cannot be passed at all, we disable it + test.disable = true; + } } return test; })]; @@ -120,14 +127,15 @@ module.exports = { /** * Serializes an XML test cases to YAML * @param {string} xmlData + * @param {string} model - model name, e.g. 'r4','stu3', 'dstu2' * @returns {string} */ - testsXmlStringToYamlString: async (xmlData) => { + testsXmlStringToYamlString: async (xmlData, model) => { const parser = new xml2js.Parser({ explicitCharkey: true }); const parseString = util.promisify(parser.parseString); const parsed = await parseString(xmlData); - const transformed = transform(parsed); + const transformed = transform(parsed, model); transformed.tests.forEach(group => { const groupKey = Object.keys(group)[0]; const groupTests = group[groupKey]; diff --git a/converter/index.js b/converter/index.js index eff5264..a5be0a9 100644 --- a/converter/index.js +++ b/converter/index.js @@ -6,14 +6,27 @@ const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); module.exports = { + /** + * Converts XML resource to the YAML format + * @param {string} from - path to XML file + * @param {string} to - path to YAML file + */ resourceXmlFileToJsonFile: async (from, to) => { const xmlData = await readFile(from); const ymlData = await convert.resourceXmlStringToJsonString(xmlData); await writeFile(to, ymlData); }, - testsXmlFileToYamlFile: async (from, to) => { + + + /** + * Converts XML test cases to the YAML format + * @param {string} from - path to XML file + * @param {string} to - path to YAML file + * @param {string} model - model name, e.g. 'r4','stu3', 'dstu2' + */ + testsXmlFileToYamlFile: async (from, to, model) => { const xmlData = await readFile(from); - const ymlData = await convert.testsXmlStringToYamlString(xmlData); + const ymlData = await convert.testsXmlStringToYamlString(xmlData, model); await writeFile(to, ymlData); } }; diff --git a/package-lock.json b/package-lock.json index d49a3b9..f2b3e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1666,20 +1666,26 @@ "dev": true }, "archiver": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", - "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-4.0.2.tgz", + "integrity": "sha512-B9IZjlGwaxF33UN4oPbfBkyA4V1SxNLeIhR1qY8sRXSsbdUkEHrrOvwlYFPx+8uQeCe9M+FG6KgO+imDmQ79CQ==", "dev": true, "requires": { "archiver-utils": "^2.1.0", - "async": "^2.6.3", + "async": "^3.2.0", "buffer-crc32": "^0.2.1", - "glob": "^7.1.4", - "readable-stream": "^3.4.0", - "tar-stream": "^2.1.0", - "zip-stream": "^2.1.2" + "glob": "^7.1.6", + "readable-stream": "^3.6.0", + "tar-stream": "^2.1.2", + "zip-stream": "^3.0.1" }, "dependencies": { + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", + "dev": true + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -2113,18 +2119,24 @@ } }, "bestzip": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/bestzip/-/bestzip-2.1.5.tgz", - "integrity": "sha512-ieNnkUNC4iF3eC8L+00Gd/niBqyjqZ28UnKYOSozWDBluqht3NMlQXLJVi47mehwgSsvGq+GqvEmVWZHQy2nrQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/bestzip/-/bestzip-2.1.7.tgz", + "integrity": "sha512-Eg5ZP0Viw1beJydZLbW246oCUnvtKGi7DhcB6IlKxP03NaxKCGVhKJD/jY4MLFRINhepfVEhAhnlc/uIxc9dHA==", "dev": true, "requires": { - "archiver": "^3.0.0", - "async": "^2.6.1", + "archiver": "^4.0.2", + "async": "^3.2.0", "glob": "^7.1.3", "which": "^1.3.1", "yargs": "^13.2.4" }, "dependencies": { + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", + "dev": true + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -2223,9 +2235,9 @@ } }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "dev": true, "requires": { "buffer": "^5.5.0", @@ -2854,15 +2866,15 @@ "dev": true }, "compress-commons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", - "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-3.0.0.tgz", + "integrity": "sha512-FyDqr8TKX5/X0qo+aVfaZ+PVmNJHJeckFBlq8jZGSJOgnynhfifoyl24qaqdUdDIBe0EVTHByN6NAkqYvE/2Xg==", "dev": true, "requires": { "buffer-crc32": "^0.2.13", "crc32-stream": "^3.0.1", "normalize-path": "^3.0.0", - "readable-stream": "^2.3.6" + "readable-stream": "^2.3.7" } }, "concat-map": { @@ -10560,12 +10572,12 @@ } }, "tar-stream": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", - "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", "dev": true, "requires": { - "bl": "^4.0.1", + "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", @@ -11901,14 +11913,14 @@ } }, "zip-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", - "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-3.0.1.tgz", + "integrity": "sha512-r+JdDipt93ttDjsOVPU5zaq5bAyY+3H19bDrThkvuVxC0xMQzU1PJcS6D+KrP3u96gH9XLomcHPb+2skoDjulQ==", "dev": true, "requires": { "archiver-utils": "^2.1.0", - "compress-commons": "^2.1.1", - "readable-stream": "^3.4.0" + "compress-commons": "^3.0.0", + "readable-stream": "^3.6.0" }, "dependencies": { "readable-stream": { diff --git a/package.json b/package.json index dec151b..b5e5c31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "2.5.0", + "version": "2.6.0", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "dependencies": { @@ -15,7 +15,7 @@ "@babel/preset-env": "^7.5.5", "babel-eslint": "^9.0.0", "babel-loader": "^8.0.6", - "bestzip": "^2.1.4", + "bestzip": "^2.1.7", "copy-webpack-plugin": "^6.0.3", "eslint": "^5.2.0", "fhir": "^4.7.10", diff --git a/src/fhirpath.js b/src/fhirpath.js index aa25552..6a52eca 100644 --- a/src/fhirpath.js +++ b/src/fhirpath.js @@ -46,8 +46,11 @@ let strings = require("./strings"); let navigation= require("./navigation"); let datetime = require("./datetime"); let logic = require("./logic"); -let types = require("./types"); -let {FP_DateTime, FP_Time, FP_Quantity, FP_Type, ResourceNode} = types; +const types = require("./types"); +const { + FP_DateTime, FP_Time, FP_Quantity, + FP_Type, ResourceNode, TypeInfo +} = types; let makeResNode = ResourceNode.makeResNode; // * fn: handler @@ -77,7 +80,9 @@ engine.invocationTable = { single: {fn: filtering.singleFn}, first: {fn: filtering.firstFn}, last: {fn: filtering.lastFn}, - ofType: {fn: filtering.ofTypeFn, arity: {1: ["Identifier"]}}, + type: {fn: types.typeFn, arity: {0: []}}, + ofType: {fn: filtering.ofTypeFn, arity: {1: ["TypeSpecifier"]}}, + is: {fn: types.isFn, arity: {1: ["TypeSpecifier"]}}, tail: {fn: filtering.tailFn}, take: {fn: filtering.takeFn, arity: {1: ["Integer"]}}, skip: {fn: filtering.skipFn, arity: {1: ["Integer"]}}, @@ -139,6 +144,7 @@ engine.invocationTable = { ">=": {fn: equality.gte, arity: {2: ["Any", "Any"]}, nullable: true}, "containsOp": {fn: collections.contains, arity: {2: ["Any", "Any"]}}, "inOp": {fn: collections.in, arity: {2: ["Any", "Any"]}}, + "isOp": {fn: types.isFn, arity: {2: ["Any", "TypeSpecifier"]}}, "&": {fn: math.amp, arity: {2: ["String", "String"]}}, "+": {fn: math.plus, arity: {2: ["Any", "Any"]}, nullable: true}, "-": {fn: math.minus, arity: {2: ["Any", "Any"]}, nullable: true}, @@ -160,6 +166,15 @@ engine.InvocationExpression = function(ctx, parentData, node) { }; engine.TermExpression = function(ctx, parentData, node) { + if (parentData) { + parentData = parentData.map((x) => { + if (x instanceof Object && x.resourceType) { + return makeResNode(x, x.resourceType); + } + return x; + }); + } + return engine.doEval(ctx,parentData, node.children[0]); }; @@ -177,6 +192,23 @@ engine.PolarityExpression = function(ctx, parentData, node) { return rtn; }; +engine.TypeSpecifier = function(ctx, parentData, node) { + let namespace, name; + const identifiers = node.text.split('.').map(i => i.replace(/(^`|`$)/g, "")); + switch (identifiers.length) { + case 2: + [namespace, name] = identifiers; + break; + case 1: + [name] = identifiers; + break; + default: + throw new Error("Expected TypeSpecifier node, got " + JSON.stringify(node)); + } + + return new TypeInfo({ namespace, name }); +}; + engine.ExternalConstantTerm = function(ctx, parentData, node) { var extConstant = node.children[0]; var identifier = extConstant.children[0]; @@ -268,8 +300,8 @@ engine.MemberInvocation = function(ctx, parentData, node ) { if (parentData) { if(util.isCapitalized(key)) { - return parentData.filter(function(x) { return x.resourceType === key; }). - map((x)=>makeResNode(x, key)); + return parentData + .filter((x) => x instanceof ResourceNode && x.path === key); } else { return parentData.reduce(function(acc, res) { res = makeResNode(res); @@ -393,9 +425,14 @@ function makeParam(ctx, parentData, type, param) { if(param.type == "TermExpression"){ return param.text; } else { - throw new Error("Expected identifier node, got ", JSON.stringify(param)); + throw new Error("Expected identifier node, got " + JSON.stringify(param)); } } + + if(type === "TypeSpecifier") { + return engine.TypeSpecifier(ctx, parentData, param); + } + var res = engine.doEval(ctx, parentData, param); if(type === "Any") { return res; @@ -559,6 +596,7 @@ engine.evalTable = { // not every evaluator is listed if they are defined on eng InvocationExpression: engine.InvocationExpression, AdditiveExpression: engine.OpExpression, MultiplicativeExpression: engine.OpExpression, + TypeExpression: engine.AliasOpExpression({"is": "isOp"}), MembershipExpression: engine.AliasOpExpression({"contains": "containsOp", "in": "inOp"}), NullLiteral: engine.NullLiteral, InvocationTerm: engine.InvocationTerm, diff --git a/src/filtering.js b/src/filtering.js index b7be9d9..fa0f133 100644 --- a/src/filtering.js +++ b/src/filtering.js @@ -4,7 +4,8 @@ /** * Adds the filtering and projection functions to the given FHIRPath engine. */ -let util = require('./utilities'); +const util = require('./utilities'); +const {TypeInfo} = require('./types'); var engine = {}; engine.whereMacro = function(parentData, expr) { @@ -74,23 +75,9 @@ engine.skipFn = function(x, num) { return x.slice(num, x.length); }; -function checkFHIRType(x, tp){ - if(typeof x === tp){ - return true; - } - if(tp === "integer") { - return Number.isInteger(x); - } - if(tp === "decimal") { - return typeof x == "number"; - } - return false; -} -// naive typeof implementation -// understand only basic types like string, number etc -engine.ofTypeFn = function(coll, type) { - return coll.filter(function(x){ - return checkFHIRType(util.valData(x), type); +engine.ofTypeFn = function(coll, typeInfo) { + return coll.filter(value => { + return TypeInfo.fromValue(value).is(typeInfo); }); }; diff --git a/src/types.js b/src/types.js index 0dec0e4..371d3f1 100644 --- a/src/types.js +++ b/src/types.js @@ -981,6 +981,21 @@ class ResourceNode { this.data = getResourceNodeData(data, path); } + /** + * Returns resource node type info. + * @return {TypeInfo} + */ + getTypeInfo() { + const namespace = TypeInfo.FHIR; + + // TODO: Here we should use property index which we will extract from the specification + + if (this.path.indexOf('.') === -1) { + return new TypeInfo({namespace, name: this.path}); + } + return TypeInfo.createByValueInNamespace({namespace, value: this.data}); + } + toJSON() { return JSON.stringify(this.data); } @@ -1014,6 +1029,105 @@ ResourceNode.makeResNode = function(data, path) { return (data instanceof ResourceNode) ? data : new ResourceNode(data, path); }; +/** + * Object class defining type information. + * Used for minimal type support. + * (see http://hl7.org/fhirpath/#types-and-reflection) + */ +class TypeInfo { + constructor({name, namespace}) { + this.name = name; + this.namespace = namespace; + } + + /** + * Checks for equality with another TypeInfo object, or that another TypeInfo + * object specifies a superclass for the type specified by this object. + * @param {TypeInfo} other + * @return {boolean} + */ + is(other) { + // TODO: Here we should use type hierarchy index which we will extract from the specification + return other instanceof TypeInfo && this.name === other.name + && (!this.namespace || !other.namespace || this.namespace === other.namespace); + } +} + +// Available namespaces: +TypeInfo.System = 'System'; +TypeInfo.FHIR = 'FHIR'; + +/** + * Creates new TypeInfo object for specified namespace and value + * @param {String} namespace + * @param {*} value + * @return {TypeInfo} + */ +TypeInfo.createByValueInNamespace = function({namespace, value}) { + let name = typeof value; + + if (Number.isInteger(value)) { + name = 'integer'; + } else if (name === "number") { + name = 'decimal'; + } else if (value instanceof FP_DateTime) { + name = 'dateTime'; + } else if (value instanceof FP_Time) { + name = 'time'; + } else if (value instanceof FP_Quantity) { + name = 'Quantity'; + } + + if (namespace === TypeInfo.System) { + name = name.replace(/^\w/, c => c.toUpperCase()); + } + + // TODO: currently can return name = 'object" or "Object" which is probably wrong + return new TypeInfo({namespace, name}) ; +}; + +/** + * Retrieves TypeInfo by value + * @param {*} value + * @return {TypeInfo} + */ +TypeInfo.fromValue = function (value) { + return value instanceof ResourceNode + ? value.getTypeInfo() + : TypeInfo.createByValueInNamespace({namespace: TypeInfo.System, value}); +}; + +/** + * Basic "type()" function implementation + * (see http://hl7.org/fhirpath/#reflection) + * @param {Array<*>} coll - input collection + * @return {Array<*>} + */ +function typeFn(coll) { + return coll.map(value => { + return TypeInfo.fromValue(value); + }); +} + +/** + * Implementation of function "is(type : type specifier)" and operator "is" + * (see http://hl7.org/fhirpath/#is-type-specifier) + * @param {Array<*>} coll - input collection + * @param {TypeInfo} typeInfo + * @return {boolean|[]} + */ +function isFn(coll, typeInfo) { + if(coll.length === 0) { + return []; + } + + if(coll.length > 1) { + throw new Error("Expected singleton on left side of is, got " + JSON.stringify(coll)); + } + + return TypeInfo.fromValue(coll[0]).is(typeInfo); +} + module.exports = { FP_Type: FP_Type, FP_TimeBase: FP_TimeBase, @@ -1022,5 +1136,8 @@ module.exports = { FP_Quantity: FP_Quantity, timeRE: timeRE, dateTimeRE: dateTimeRE, - ResourceNode: ResourceNode + ResourceNode: ResourceNode, + TypeInfo: TypeInfo, + typeFn, + isFn }; diff --git a/test/cases/5.2_filtering_and_projection.yaml b/test/cases/5.2_filtering_and_projection.yaml index 6d1603b..77e2484 100644 --- a/test/cases/5.2_filtering_and_projection.yaml +++ b/test/cases/5.2_filtering_and_projection.yaml @@ -93,7 +93,7 @@ tests: result: [1] - desc: '** decimal' expression: heteroattr.ofType(decimal) - result: [1, 1.01] + result: [1.01] - desc: '** boolean' expression: heteroattr.ofType(boolean) result: [true, false] diff --git a/test/cases/6.3_types.yaml b/test/cases/6.3_types.yaml index 003e76c..606da81 100644 --- a/test/cases/6.3_types.yaml +++ b/test/cases/6.3_types.yaml @@ -3,20 +3,41 @@ tests: - desc: '6.3.1. is' #If the left operand is a collection with a single item and the second operand is a type identifier, this operator returns true if the type of the left operand is the type specified in the second operand, or a subclass thereof. If the identifier cannot be resolved to a valid type identifier, the evaluator will throw an error. If the input collections contains more than one item, the evaluator will throw an error. In all other cases this function returns the empty collection. - desc: '** is boolean' - expression: 1 > 2 is boolean - disable: true + expression: 1 > 2 is Boolean result: [true] - desc: '** values is integer' - expression: coll.all($this is number) - disable: true + expression: coll.a is integer result: [true] - desc: '** values isnt Patient type' expression: coll.all($this is Patient) - disable: true result: [false] + - desc: '** is Quantity' + inputfile: patient-example.json + expression: 1 year is Quantity + result: + - true + + - desc: '** is System.Quantity' + inputfile: patient-example.json + expression: 1 year is System.Quantity + result: + - true + + - desc: '** is(Quantity)' + inputfile: patient-example.json + expression: 1 day.is(Quantity) + result: + - true + + - desc: '** is(System.Quantity)' + inputfile: patient-example.json + expression: 1 day.is(System.Quantity) + result: + - true + - desc: '6.3.2. as' #If the left operand is a collection with a single item and the second operand is an identifier, this function returns the value of the left operand if it is of the type specified in the second operand, or a subclass thereof. If the identifier cannot be resolved to a valid type identifier, the evaluator will throw an error. If there is more than one item in the input collection, the evaluator will throw an error. Otherwise, this operator returns the empty collection. diff --git a/test/cases/fhir-r4.yaml b/test/cases/fhir-r4.yaml index 8ae750d..f994322 100644 --- a/test/cases/fhir-r4.yaml +++ b/test/cases/fhir-r4.yaml @@ -76,35 +76,42 @@ tests: expression: Observation.value.unit result: - lbs + disable: true - desc: '** testPolymorphismB' inputfile: observation-example.json expression: Observation.valueQuantity.unit error: true + disable: true - desc: '** testPolymorphismIsA' inputfile: observation-example.json expression: Observation.value.is(Quantity) result: - true + model: r4 - desc: '** testPolymorphismIsA' inputfile: observation-example.json expression: Observation.value is Quantity result: - true + model: r4 - desc: '** testPolymorphismIsB' inputfile: observation-example.json expression: Observation.value.is(Period).not() result: - true + model: r4 - desc: '** testPolymorphismAsA' inputfile: observation-example.json expression: Observation.value.as(Quantity).unit result: - lbs + disable: true - desc: '** testPolymorphismAsAFunction' inputfile: observation-example.json expression: (Observation.value as Quantity).unit result: - lbs + disable: true - desc: '** testPolymorphismAsB' inputfile: observation-example.json expression: (Observation.value as Period).unit @@ -113,7 +120,7 @@ tests: inputfile: observation-example.json expression: Observation.value.as(Period).start result: [] - disable: true + disable: true - 'group: testDollar': - desc: '** testDollarThis1' inputfile: patient-example.json @@ -276,69 +283,61 @@ tests: expression: '@2015-02-04T14.is(DateTime)' result: - true - disable: true - desc: '** testLiteralDateTimeMinute' inputfile: patient-example.json expression: '@2015-02-04T14:34.is(DateTime)' result: - true - disable: true - desc: '** testLiteralDateTimeSecond' inputfile: patient-example.json expression: '@2015-02-04T14:34:28.is(DateTime)' result: - true - disable: true - desc: '** testLiteralDateTimeMillisecond' inputfile: patient-example.json expression: '@2015-02-04T14:34:28.123.is(DateTime)' result: - true - disable: true - desc: '** testLiteralDateTimeUTC' inputfile: patient-example.json expression: '@2015-02-04T14:34:28Z.is(DateTime)' result: - true - disable: true - desc: '** testLiteralDateTimeTimezoneOffset' inputfile: patient-example.json expression: '@2015-02-04T14:34:28+10:00.is(DateTime)' result: - true - disable: true - desc: '** testLiteralTimeHour' inputfile: patient-example.json expression: '@T14.is(Time)' result: - true - disable: true - desc: '** testLiteralTimeMinute' inputfile: patient-example.json expression: '@T14:34.is(Time)' result: - true - disable: true - desc: '** testLiteralTimeSecond' inputfile: patient-example.json expression: '@T14:34:28.is(Time)' result: - true - disable: true - desc: '** testLiteralTimeMillisecond' inputfile: patient-example.json expression: '@T14:34:28.123.is(Time)' result: - true - disable: true - desc: '** testLiteralTimeUTC' inputfile: patient-example.json expression: '@T14:34:28Z.is(Time)' error: true + disable: true - desc: '** testLiteralTimeTimezoneOffset' inputfile: patient-example.json expression: '@T14:34:28+10:00.is(Time)' error: true + disable: true - desc: '** testLiteralQuantityDecimal' inputfile: patient-example.json expression: 10.1 'mg'.convertsToQuantity() @@ -404,25 +403,25 @@ tests: expression: Observation.value.value > 180.0 result: - true - disable: true + model: r4 - desc: '** testLiteralDecimalGreaterThanZeroTrue' inputfile: observation-example.json expression: Observation.value.value > 0.0 result: - true - disable: true + model: r4 - desc: '** testLiteralDecimalGreaterThanIntegerTrue' inputfile: observation-example.json expression: Observation.value.value > 0 result: - true - disable: true + model: r4 - desc: '** testLiteralDecimalLessThanInteger' inputfile: observation-example.json expression: Observation.value.value < 190 result: - true - disable: true + model: r4 - desc: '** testLiteralDecimalLessThanInvalid' inputfile: observation-example.json expression: Observation.value.value < 'test' @@ -654,13 +653,11 @@ tests: expression: 1.is(Integer) result: - true - disable: true - desc: '** testIntegerLiteralIsSystemInteger' inputfile: patient-example.json expression: 1.is(System.Integer) result: - true - disable: true - desc: '** testStringLiteralConvertsToInteger' inputfile: patient-example.json expression: '''1''.convertsToInteger()' @@ -681,7 +678,6 @@ tests: expression: '''1''.is(Integer).not()' result: - true - disable: true - desc: '** testBooleanLiteralConvertsToInteger' inputfile: patient-example.json expression: true.convertsToInteger() @@ -692,13 +688,11 @@ tests: expression: true.is(Integer).not() result: - true - disable: true - desc: '** testDateIsNotInteger' inputfile: patient-example.json expression: '@2013-04-05.is(Integer).not()' result: - true - disable: true - desc: '** testIntegerLiteralToInteger' inputfile: patient-example.json expression: 1.toInteger() = 1 @@ -733,7 +727,6 @@ tests: expression: 1.is(Decimal).not() result: - true - disable: true - desc: '** testDecimalLiteralConvertsToDecimal' inputfile: patient-example.json expression: 1.0.convertsToDecimal() @@ -755,7 +748,6 @@ tests: expression: '''1''.is(Decimal).not()' result: - true - disable: true - desc: '** testStringLiteralConvertsToDecimalFalse' inputfile: patient-example.json expression: '''1.a''.convertsToDecimal().not()' @@ -772,7 +764,6 @@ tests: expression: '''1.0''.is(Decimal).not()' result: - true - disable: true - desc: '** testBooleanLiteralConvertsToDecimal' inputfile: patient-example.json expression: true.convertsToDecimal() @@ -783,7 +774,6 @@ tests: expression: true.is(Decimal).not() result: - true - disable: true - desc: '** testIntegerLiteralToDecimal' inputfile: patient-example.json expression: 1.toDecimal() = 1.0 @@ -819,7 +809,6 @@ tests: expression: 1.is(Quantity).not() result: - true - disable: true - desc: '** testDecimalLiteralConvertsToQuantity' inputfile: patient-example.json expression: 1.0.convertsToQuantity() @@ -830,7 +819,6 @@ tests: expression: 1.0.is(System.Quantity).not() result: - true - disable: true - desc: '** testStringIntegerLiteralConvertsToQuantity' inputfile: patient-example.json expression: '''1''.convertsToQuantity()' @@ -841,7 +829,6 @@ tests: expression: '''1''.is(System.Quantity).not()' result: - true - disable: true - desc: '** testStringQuantityLiteralConvertsToQuantity' inputfile: patient-example.json expression: '''1 day''.convertsToQuantity()' @@ -872,7 +859,6 @@ tests: expression: '''1.0''.is(System.Quantity).not()' result: - true - disable: true - desc: '** testBooleanLiteralConvertsToQuantity' inputfile: patient-example.json expression: true.convertsToQuantity() @@ -883,7 +869,6 @@ tests: expression: true.is(System.Quantity).not() result: - true - disable: true - desc: '** testIntegerLiteralToQuantity' inputfile: patient-example.json expression: 1.toQuantity() = 1 '1' @@ -1004,7 +989,6 @@ tests: expression: 1.is(String).not() result: - true - disable: true - desc: '** testNegativeIntegerLiteralConvertsToString' inputfile: patient-example.json expression: (-1).convertsToString() @@ -1903,7 +1887,7 @@ tests: expression: 'Observation.value = 185 ''[lb_av]''' result: - true - disable: true + model: r4 - 'group: testNEquality': - desc: '** testNEquality1' inputfile: patient-example.json @@ -2025,7 +2009,7 @@ tests: expression: Observation.value != 185 'kg' result: - true - disable: true + model: r4 - 'group: testEquivalent': - desc: '** testEquivalent1' inputfile: patient-example.json @@ -2144,7 +2128,7 @@ tests: expression: 'Observation.value ~ 185 ''[lb_av]''' result: - true - disable: true + model: r4 - 'group: testNotEquivalent': - desc: '** testNotEquivalent1' inputfile: patient-example.json @@ -2264,7 +2248,7 @@ tests: expression: Observation.value !~ 185 'kg' result: - true - disable: true + model: r4 - 'group: testLessThan': - desc: '** testLessThan1' inputfile: patient-example.json @@ -3434,13 +3418,11 @@ tests: expression: 1 > 2 is Boolean result: - true - disable: true - desc: '** testPrecedence4' inputfile: patient-example.json expression: 1 | 1 is Integer result: - true - disable: true - 'group: testVariables': - desc: '** testVariables1' inputfile: patient-example.json @@ -3602,7 +3584,6 @@ tests: expression: Patient.ofType(FHIR.`Patient`).type().name result: - Patient - disable: true - 'group: testConformsTo': - desc: '** testConformsTo' inputfile: patient-example.json diff --git a/test/convert.test.js b/test/convert.test.js index 844fc0f..5e00cb8 100644 --- a/test/convert.test.js +++ b/test/convert.test.js @@ -11,7 +11,7 @@ const resourceYamlString = fs.readFileSync(__dirname + '/fixtures/resource-examp describe('Converter base test', () => { it('Convert a simple tests from XML to YAML', async () => { loadResource('resource-example.json', __dirname + '/fixtures/resource-example.json'); - const data = await convert.testsXmlStringToYamlString(testXmlString); + const data = await convert.testsXmlStringToYamlString(testXmlString, 'r4'); expect(data).toEqual(testYamlString); });