Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation for simple XPaths in form config #486

Merged
merged 12 commits into from
Jul 12, 2022
55 changes: 30 additions & 25 deletions package-lock.json

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

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"eslint": "eslint 'src/**/*.js' test/*.js 'test/**/*.js'",
"docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB",
"docker-stop-couchdb": "docker stop cht-conf-couchdb || true",
"test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only ../../test/**/*.spec.js && cd ../.. && npm run docker-stop-couchdb",
"test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb",
Copy link
Contributor Author

@jkuester jkuester Jul 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I needed these extra quotes in here to get mocha to run the tests nested down in test/lib/validation/form

"semantic-release": "semantic-release"
},
"bin": {
Expand All @@ -38,6 +38,7 @@
"@hapi/joi": "^16.1.8",
"@medic/translation-checker": "^1.0.1",
"@parcel/watcher": "^2.0.5",
"@xmldom/xmldom": "^0.8.2",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, @xmldom/xmldom is just the new version of xmldom.

"canonical-json": "0.0.4",
"csv-parse": "^4.16.0",
"dom-compare": "^0.6.0",
Expand Down Expand Up @@ -67,7 +68,7 @@
"terser-webpack-plugin": "^1.4.3",
"uuid": "^8.3.2",
"webpack": "^4.46.0",
"xmldom": "^0.6.0"
"xpath": "0.0.32"
},
"devDependencies": {
"@commitlint/cli": "^13.2.0",
Expand Down
56 changes: 47 additions & 9 deletions src/lib/forms-utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
const xpath = require('xpath');
const fs = require('./sync-fs');

const XPATH_MODEL = '/h:html/h:head/model';

const getNode = (currentNode, path) =>
xpath.parse(path).select1({ node: currentNode, allowAnyNamespaceForNoPrefix: true });

const getNodes = (currentNode, path) =>
xpath.parse(path).select({ node: currentNode, allowAnyNamespaceForNoPrefix: true });

module.exports = {
/**
* Get the full path of the form, or null if the path doesn't exist.
Expand Down Expand Up @@ -29,32 +38,61 @@ module.exports = {
};
},

// This isn't really how to parse XML, but we have fairly good control over the
// input and this code is working so far. This may break with changes to the
// formatting of output from xls2xform.
/**
* Returns the node from the form XML specified by the given XPath.
* @param {Element} currentNode the current node in the form XML document
* @param {string} path the XPath expression
* @returns {Element} the selected node or `undefined` if not found
*/
getNode,
jkuester marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns the nodes from the form XML specified by the given XPath.
* @param {Element} currentNode the current node in the form XML document
* @param {string} path the XPath expression
* @returns {Element} the selected nodes or an empty array if none are found
*/
getNodes,

/**
* Returns the `bind` nodes for the given form XML.
* @param {Document} xmlDoc the form XML document
* @returns {Element}
*/
getBindNodes: xmlDoc => getNodes(xmlDoc, `${XPATH_MODEL}/bind`),

/**
* Returns the primary (first) `instance` node for the given form XML.
* @param {Document} xmlDoc the form XML document
* @returns {Element}
*/
getPrimaryInstanceNode: xmlDoc => getNode(xmlDoc, `${XPATH_MODEL}/instance`),

/**
* Check whether the XForm has the <instanceID/> tag.
* @param {string} xml the XML string
* @param {string} xmlDoc the XML document
* @returns {boolean}
*/
formHasInstanceId: xml => xml.includes('<instanceID/>'),
formHasInstanceId: xmlDoc => getNode(xmlDoc, `//meta/instanceID`) !== undefined,

// This isn't really how to parse XML, but we have fairly good control over the
// input and this code is working so far. This may break with changes to the
// formatting of output from xls2xform.
/**
* Get the title string inside the <h:title> tag
* @param {string} xml the XML string
* @returns {string}
*/
readTitleFrom: xml =>
xml.substring(xml.indexOf('<h:title>') + 9, xml.indexOf('</h:title>')),
xml.substring(xml.indexOf('<h:title>') + 9, xml.indexOf('</h:title>')),

/**
* Get the ID of the form
* @param {string} xml the XML string
* @returns {string}
*/
readIdFrom: xml =>
xml.match(/<model>[^]*<\/model>/)[0]
.match(/<instance>[^]*<\/instance>/)[0]
.match(/id="([^"]*)"/)[1],
xml.match(/<model>[^]*<\/model>/)[0]
.match(/<instance>[^]*<\/instance>/)[0]
.match(/id="([^"]*)"/)[1],
};
4 changes: 3 additions & 1 deletion src/lib/validate-forms.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { DOMParser } = require('@xmldom/xmldom');
const argsFormFilter = require('./args-form-filter');
const environment = require('./environment');
const fs = require('./sync-fs');
Expand All @@ -7,6 +8,7 @@ const {
getFormFilePaths
} = require('./forms-utils');

const domParser = new DOMParser();
const VALIDATIONS_PATH = fs.path.resolve(__dirname, './validation/form');
const validations = fs.readdir(VALIDATIONS_PATH)
.filter(name => name.endsWith('.js'))
Expand Down Expand Up @@ -52,7 +54,7 @@ module.exports = async (projectDir, subDirectory, options={}) => {
const { xformPath } = getFormFilePaths(formsDir, fileName);
const xml = fs.read(xformPath);

const valParams = { xformPath, xmlStr: xml };
const valParams = { xformPath, xmlStr: xml, xmlDoc: domParser.parseFromString(xml) };
for(const validation of validations) {
if(validation.requiresInstance && !instanceProvided) {
validationSkipped = true;
Expand Down
Loading