Skip to content

Commit

Permalink
Merge pull request #13 in LFOR/fhirpath.js from feature/LF-1485/updat…
Browse files Browse the repository at this point in the history
…e-test-converter to master

* commit 'ed74194ff0c1ecc4d74871eb9e4388134c21f647':
  Update FHIRPath test converter
  Update FHIRPath test converter
  Update FHIRPath test converter
  • Loading branch information
plynchnlm committed Jun 9, 2020
2 parents 8da56ba + ed74194 commit 01c7e18
Show file tree
Hide file tree
Showing 24 changed files with 3,865 additions and 3,340 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [2.2.2] - 2020-06-05
### Fixed
- Updated FHIRPath test cases

## [2.2.1] - 2020-06-03
### Fixed
- Issue with substring function without a second parameter
Expand Down
9 changes: 4 additions & 5 deletions converter/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# FHIR-r4 test converter

CLI-tool for converting fhirpath [XML test cases](https://github.com/hl7-fhir/fhir-svn/blob/master/tests/resources/tests-fhir-r4.xml)
into fhirpath.js yaml test cases format.
CLI-tool for converting fhirpath [XML test cases](https://github.com/HL7/FHIRPath/tree/master/tests/r4/)
into fhirpath.js yaml test cases format with json resource files.

### Installing:
```npm ci```


### Convert test file:
```converter/bin/index.js converter/dataset/fhir-r4.xml(path to xml file) converter/dataset/tests-r4.yaml(path to save result data)```
### Download and convert test file and resource files:
```converter/bin/index.js```

### Converter test in whole test scope:
```npm run test```
72 changes: 67 additions & 5 deletions converter/bin/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,74 @@
#! /usr/bin/env node

// Directory for downloading XML source files
const sourceDir = __dirname + '/../dataset/';
// Directory for converter output(for YAML and JSON files)
const destDir = __dirname + '/../../test/';

// Descriptions for file generation:
// [<relative path to XML source file>, <relative path to YAML/JSON output file>, <download URL>]
const sources = [
['fhir-r4.xml', 'cases/fhir-r4.yaml', 'https://raw.githubusercontent.com/HL7/FHIRPath/master/tests/r4/tests-fhir-r4.xml'],
['input/observation-example.xml', 'resources/observation-example.json', 'https://raw.githubusercontent.com/HL7/FHIRPath/master/tests/r4/input/observation-example.xml'],
['input/patient-example.xml', 'resources/patient-example.json', 'https://raw.githubusercontent.com/HL7/FHIRPath/master/tests/r4/input/patient-example.xml'],
['input/questionnaire-example.xml', 'resources/questionnaire-example.json', 'https://raw.githubusercontent.com/HL7/FHIRPath/master/tests/r4/input/questionnaire-example.xml'],
['input/valueset-example-expansion.xml', 'resources/valueset-example-expansion.json', 'https://raw.githubusercontent.com/HL7/FHIRPath/master/tests/r4/input/valueset-example-expansion.xml']
];

const commander = require('commander');
const run = require('../index');
const convert = require('../index');

const https = require('https');
const fs = require('fs');

function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https.get(url, function(res) {
const { statusCode } = res;

if (statusCode !== 200) {
// Consume response data to free up memory
res.resume();
reject(`Failed to download ${url}.\n` +
`Status Code: ${statusCode}`);
} else {
res.pipe(file);
file.on('finish', function () {
resolve();
});
}
});
});
}

commander
.arguments('<path_from> <path_to>')
.description('Convert xml test cases to yaml')
.action(async (from, to) =>
await run(from, to))
.option('-s, --skip-download', 'skip downloading sources from FHIRPath repository')
.description('Convert xml test cases/resources to yaml/json')
.action(async (cmd) => {
try {
if (!cmd.skipDownload) {
for (let i = 0; i < sources.length; i++) {
const [xmlFilename,, url] = sources[i];
await downloadFile(url, sourceDir + xmlFilename);
}
}

const resourceFiles = sources.filter(([,target]) => target.endsWith('.json'));
const testFiles = sources.filter(([,target]) => target.endsWith('.yaml'));

for (let i = 0; i < resourceFiles.length; i++) {
const [xmlFilename, jsonFilename] = resourceFiles[i];
await convert.resourceXmlFileToJsonFile(sourceDir + xmlFilename, destDir + jsonFilename);
}

for (let i = 0; i < testFiles.length; i++) {
const [xmlFilename, yamlFilename] = testFiles[i];
await convert.testsXmlFileToYamlFile(sourceDir + xmlFilename, destDir + yamlFilename);
}
} catch(e) {
console.error(e);
}
})
.parse(process.argv);

99 changes: 87 additions & 12 deletions converter/converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,44 @@ const _ = require('lodash');
const xml2js = require('xml2js');
const yaml = require('js-yaml');

module.exports = async (xmlData) => {
const parser = new xml2js.Parser({ explicitCharkey: true });
const parseString = util.promisify(parser.parseString);
const fhir = new (require('fhir').Fhir);
const fhirpath = require('../src/fhirpath');
const { getFHIRModel, calcExpression } = require("../test/test_utils");
const FP_DateTime = require('../src/types').FP_DateTime;
const equals = _.isEqual;

const parsed = await parseString(xmlData);
const transformed = transform(parsed);
const formatted = yaml.dump(transformed);
return formatted;
};
/**
* Validates a test to determine whether to disable it
* @param {Object} test
* @return {boolean}
*/
function validateTest(test) {
if (!test.error && test.expression) {
try {
const result = calcExpression(test.expression, test);
// Run the result through JSON so the FP_Type quantities get converted to
// strings. Also , if the result is an FP_DateTime, convert to a Date
// object so that timezone differences are handled.
if (result.length === 1 && result[0] instanceof FP_DateTime)
return equals(new Date(result[0]), new Date(test.result[0]));
else
return equals(JSON.parse(JSON.stringify(result)), test.result);
} catch (e) {
return false;
}
}
else if (test.error) {
let exception = null;
try {
fhirpath.evaluate({}, test.expression, null,
getFHIRModel(test.model));
}
catch (error) {
exception = error;
}
return exception !== null;
}
}

const mapper = {
boolean: (v) => v === 'true',
Expand All @@ -25,24 +54,38 @@ const mapper = {

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
* @return {Object}
*/
const transform = (node) => {

return Object.keys(node).reduce((acc, key) => {

switch(key) {
case 'tests':
return { tests: transform(node[key]) };
return { tests: transform(_.pick(node[key], 'group')) };

case 'group':
return [...acc, ...node[key].map(item =>
({ [`group: ${item['$'].name}`]: transform(_.pick(item, 'test')) }))];
({ [`group: ${item['$'].description || item['$'].name}`]: transform(_.pick(item, 'test')) }))];

case 'test':
return [...acc, ...node[key].map(item => transform(item))];
return [...acc, ...node[key].map(item => {
let test = transform(item);
if (!test.hasOwnProperty('result') && !test.error) {
test.result = [];
}
if (!validateTest(test)) {
test.disable = true;
}
return test;
})];

case '$': {
const value = node[key];
const updated = { desc: `** ${node[key].name || 'test'}` };
const updated = { desc: `** ${node[key].description || node[key].name || 'test'}` };
if (_.has(value, 'inputfile')) {
updated.inputfile = value.inputfile.replace(/.xml$/, '.json');
}
Expand All @@ -64,3 +107,35 @@ const transform = (node) => {
}
}, []);
};

module.exports = {
/**
* Serializes an XML resource to JSON
* @param {string} xmlData
* @returns {string}
*/
resourceXmlStringToJsonString: async (xmlData) => {
return fhir.xmlToJson(xmlData).replace(/\t/g, ' ');
},
/**
* Serializes an XML test cases to YAML
* @param {string} xmlData
* @returns {string}
*/
testsXmlStringToYamlString: async (xmlData) => {
const parser = new xml2js.Parser({ explicitCharkey: true });
const parseString = util.promisify(parser.parseString);

const parsed = await parseString(xmlData);
const transformed = transform(parsed);
transformed.tests.forEach(group => {
const groupKey = Object.keys(group)[0];
const groupTests = group[groupKey];
if (groupTests.some(test => test.disable) && groupTests.every(test => test.disable || test.error)) {
group.disable = true;
groupTests.forEach(test => delete test.disable);
}
});
return yaml.dump(transformed);
}
};
Loading

0 comments on commit 01c7e18

Please sign in to comment.