diff --git a/CHANGELOG.md b/CHANGELOG.md index c726c52..f587f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -Unreleased ---------------------- +[1.4.0] - 2018-06-13 +-------------------- ##### Added -- Version property when used as CommonJS module +- Version property when used as CommonJS module. +- Validation for appearances that depend on other appearances. ##### Changed - Ignore deprecated appearance usage errors in --oc mode. @@ -13,6 +14,7 @@ Unreleased ##### Fixed - Analog-scale appearance outputs warning. +- False error for repeat without ref attribute. [1.3.0] - 2018-06-07 --------------------- diff --git a/package-lock.json b/package-lock.json index 5b33437..60e7010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "enketo-validate", - "version": "1.2.2", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -822,7 +822,7 @@ }, "enketo-xpath-extensions-oc": { "version": "git+https://github.com/OpenClinica/enketo-xpath-extensions-oc.git#a30e5f9cf87b15c07d57e9412019737413b0b907", - "from": "enketo-xpath-extensions-oc@git+https://github.com/OpenClinica/enketo-xpath-extensions-oc.git#a30e5f9cf87b15c07d57e9412019737413b0b907" + "from": "git+https://github.com/OpenClinica/enketo-xpath-extensions-oc.git#a30e5f9" }, "enketo-xpathjs": { "version": "1.8.0", diff --git a/package.json b/package.json index e71087a..3a2bc44 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "enketo-validate", - "version": "1.3.0", + "version": "1.4.0", "description": "An XForm validator around Enketo's form engine", "main": "src/validator.js", + "bin": "./validate", "scripts": { "test": "mocha test/spec/*.spec.js", "install": "browserify src/FormModel.js > build/FormModel-bundle.js" @@ -37,4 +38,4 @@ "mocha": "^5.0.1", "pkg": "^4.3.0" } -} \ No newline at end of file +} diff --git a/src/appearances.json b/src/appearances.json index 4c01862..5a1249f 100644 --- a/src/appearances.json +++ b/src/appearances.json @@ -121,5 +121,11 @@ "controls": [ "input" ], "types": [ "decimal", "xsd:decimal", "int", "xsd:int" ] }, - "no-ticks": "analog-scale" + "no-ticks": { + "appearances": [ "analog-scale" ] + }, + "comment": { + "types": [ "string", "xsd:string" ] + }, + "dn": "comment" } \ No newline at end of file diff --git a/src/xform.js b/src/xform.js index 886f101..4c19dd3 100644 --- a/src/xform.js +++ b/src/xform.js @@ -237,38 +237,41 @@ class XForm { if ( typeof rules === 'string' ) { rules = appearanceRules[ rules ]; } - const ref = control.getAttribute( 'ref' ); + const controlNsPrefix = this.nsPrefixResolver( control.namespaceURI ); + const controlName = controlNsPrefix && /:/.test( control.nodeName ) ? controlNsPrefix + ':' + control.nodeName.split( ':' )[ 1 ] : control.nodeName; + const pathAttr = controlName === 'repeat' ? 'nodeset' : 'ref'; + const ref = control.getAttribute( pathAttr ); if ( !ref ) { - errors.push( 'Question found in body that has no ref attribute' ); + errors.push( `Question found in body that has no ${pathAttr} attribute (${control.nodeName}).` ); return; } const nodeName = ref.substring( ref.lastIndexOf( '/' ) + 1 ); // in model! - const controlNsPrefix = this.nsPrefixResolver( control.namespaceURI ); const bindEl = this.bind( ref ); - const controlName = controlNsPrefix && /:/.test( control.nodeName ) ? controlNsPrefix + ':' + control.nodeName.split( ':' )[ 1 ] : control.nodeName; let dataType = bindEl ? bindEl.getAttribute( 'type' ) : 'string'; // Convert ns prefix to properly evaluate XML Schema datatypes regardless of namespace prefix used in XForm. const typeValNs = /:/.test( dataType ) ? bindEl.lookupNamespaceURI( dataType.split( ':' )[ 0 ] ) : null; dataType = typeValNs ? `${this.nsPrefixResolver(typeValNs)}:${dataType.split(':')[1]}` : dataType; if ( !rules ) { - warnings.push( `Appearance "${appearance}" for question "${nodeName}" is not supported` ); + warnings.push( `Appearance "${appearance}" for question "${nodeName}" is not supported.` ); return; } if ( rules.controls && !rules.controls.includes( controlName ) ) { - warnings.push( `Appearance "${appearance}" for question "${nodeName}" is not valid for this question type (${control.nodeName})` ); + warnings.push( `Appearance "${appearance}" for question "${nodeName}" is not valid for this question type (${control.nodeName}).` ); return; } if ( rules.types && !rules.types.includes( dataType ) ) { // Only check types if controls check passed. // TODO check namespaced types when it becomes applicable (for XML Schema types). - warnings.push( `Appearance "${appearance}" for question "${nodeName}" is not valid for this data type (${dataType})` ); + warnings.push( `Appearance "${appearance}" for question "${nodeName}" is not valid for this data type (${dataType}).` ); + return; + } + if ( rules.appearances && !rules.appearances.some( appearanceMatch => appearances.includes( appearanceMatch ) ) ) { + warnings.push( `Appearance "${appearance}" for question "${nodeName}" requires any of these appearances: ${rules.appearances}.` ); return; } - // TODO: if an appearance is only valid when another appearance is used (e.g. no-ticks) - // switched off when warnings are output as errors (for OC) - may need different approach if ( rules.preferred && warnings !== errors ) { - warnings.push( `Appearance "${appearance}" for question "${nodeName}" is deprecated, use "${rules.preferred}" instead` ); + warnings.push( `Appearance "${appearance}" for question "${nodeName}" is deprecated, use "${rules.preferred}" instead.` ); } // Possibilities for future additions: // - check accept/mediaType diff --git a/test/spec/xform.spec.js b/test/spec/xform.spec.js index ac2821e..5c16044 100644 --- a/test/spec/xform.spec.js +++ b/test/spec/xform.spec.js @@ -108,7 +108,7 @@ describe( 'XForm', () => { const xf = loadXForm( 'appearances.xml' ); const result = validator.validate( xf ); const resultOc = validator.validate( xf, { openclinica: true } ); - const ISSUES = 13; + const ISSUES = 14; it( 'outputs warnings', () => { expect( result.warnings.length ).to.equal( ISSUES ); @@ -122,6 +122,7 @@ describe( 'XForm', () => { expect( arrContains( result.warnings, /"numbers" for question "g"/i ) ).to.equal( true ); expect( arrContains( result.warnings, /"horizontal-compact" for question "k" .+ deprecated.+"compact"/i ) ).to.equal( true ); expect( arrContains( result.warnings, /"field-list" for question "two"/i ) ).to.equal( true ); + expect( arrContains( result.warnings, /"no-ticks" for question "g"/i ) ).to.equal( true ); } ); it( 'outputs no errors', () => { diff --git a/test/xform/appearances.xml b/test/xform/appearances.xml index 2addbba..6d7abdb 100644 --- a/test/xform/appearances.xml +++ b/test/xform/appearances.xml @@ -74,7 +74,7 @@ - + @@ -99,6 +99,6 @@ - + \ No newline at end of file diff --git a/validate b/validate index 128c0d7..9fb39c6 100755 --- a/validate +++ b/validate @@ -22,7 +22,11 @@ const _getFileContents = filePath => { } ); }; -const _output = ( issues = [], error = false ) => console[ error ? 'error' : 'log' ]( `${issues.join( '\n\n' )}` ); +const _output = ( issues = [], error = false ) => { + if ( issues.length ) { + console[ error ? 'error' : 'log' ]( `\n\n${issues.join( '\n\n' )}` ); + } +}; program .usage( '[options] ' ) @@ -53,10 +57,10 @@ if ( program.me ) { _output( result.errors, true ); if ( result.errors.length ) { - _output( [ options.openclinica ? '' : '\n\nResult: Invalid\n\n' ], true ); + _output( [ options.openclinica ? '' : 'Result: Invalid\n\n' ], true ); process.exit( 1 ); } else { - _output( [ options.openclinica ? '' : '\n\n>> XForm is valid! See above for any warnings.\n\n' ] ); + _output( [ options.openclinica ? '' : '>> XForm is valid! See above for any warnings.\n\n' ] ); process.exit( 0 ); } } );