diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js
new file mode 100644
index 0000000..56141c7
--- /dev/null
+++ b/src/document/DocumentProgramFormatter.js
@@ -0,0 +1,218 @@
+/**
+ * Formats program bucket data
+ *
+ * Copyright (C) 2018 R-T Specialty, LLC.
+ *
+ * This file is part of the Liza Data Collection Framework.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+"use strict";
+
+const Class = require( 'easejs' ).Class;
+
+
+/**
+ * Formats program bucket data
+ *
+ * This takes a document and formats the bucket data in a
+ * structured manner of steps, groups and fields metadata
+ */
+module.exports = Class( 'DocumentProgramFormatter',
+{
+ /**
+ * Current program
+ *
+ * @type {Program}
+ */
+ 'private _program': null,
+
+ /**
+ * Performs classification matching on fields
+ *
+ * A field will have a positive match for a given index if all of its
+ * classes match
+ *
+ * @type {FieldClassMatcher}
+ */
+ 'private _class_matcher': null,
+
+
+ /**
+ * Initialize document formatter
+ *
+ * @param {Program} program active program
+ * @param {FieldClassMatcher} class_matcher class/field matcher
+ */
+ constructor( program, class_matcher )
+ {
+ this._program = program;
+ this._class_matcher = class_matcher;
+ },
+
+
+ /**
+ * Formats document data into structure similar to program
+ *
+ * @param {Bucket} bucket document bucket
+ *
+ * @return {Promise.} a promise of a data object
+ */
+ 'public format'( bucket )
+ {
+ return new Promise( ( resolve, reject ) =>
+ {
+ const cmatch = this._program.classify( bucket.getData() );
+
+ this._class_matcher.match( cmatch, ( matches ) =>
+ {
+ const len = this._program.steps.length;
+ const data = this._parseSteps( len, bucket, matches );
+
+ resolve( data );
+ } );
+
+ } );
+ },
+
+
+ /**
+ * Parses step data
+ *
+ * @param {Number} len step length
+ * @param {Bucket} bucket document bucket
+ * @param {Object} matches field matches
+ *
+ * @return {Object} step data
+ */
+ 'private _parseSteps'( len, bucket, matches )
+ {
+ const data = { steps: [] };
+
+ for ( let i = 1; i < len; i++ )
+ {
+ const step = {};
+ const step_groups = this._program.steps[ i ].groups;
+
+ const groups = this._parseGroups( step_groups, bucket, matches );
+
+ step.title = this._program.steps[ i ].title;
+ step.groups = groups;
+
+ data.steps.push( step );
+ }
+
+ return data;
+ },
+
+
+ /**
+ * Parses group data
+ *
+ * @param {Array} step_groups array of group data
+ * @param {Bucket} bucket document bucket
+ * @param {Object} matches field matches
+ *
+ * @return {Array} array of groups
+ */
+ 'private _parseGroups'( step_groups, bucket, matches )
+ {
+ return step_groups.map( group_id =>
+ {
+ const fields = this._program.groupExclusiveFields[ group_id ];
+ const { title, link } = this._program.groups[ group_id ];
+
+ return {
+ id: group_id,
+ title: title || "",
+ link: link || "",
+ questions: this._parseFields( fields, bucket, matches ),
+ };
+ } );
+ },
+
+
+ /**
+ * Parses fields/question data
+ *
+ * @param {Array} fields array of field data
+ * @param {Bucket} bucket document bucket
+ * @param {Object} matches field matches
+ *
+ * @return {Array} array of questions
+ */
+ 'private _parseFields'( fields, bucket, matches )
+ {
+ const questions = [];
+
+ for ( let field in fields )
+ {
+ const field_id = fields[ field ];
+
+ // Don't include fields that are not in program.fields
+ if ( typeof this._program.fields[ field_id ] === "undefined" )
+ {
+ continue;
+ }
+
+ const value = bucket.getDataByName( field_id );
+ const { label, type } = this._program.fields[ field_id ];
+
+ questions.push( {
+ id: field_id,
+ label: label || "",
+ value: value,
+ type: type || "",
+ applicable: this._getApplicable( matches, field_id, value ),
+ } );
+
+ }
+
+ return questions;
+ },
+
+
+ /**
+ * Determine when a field is shown by index
+ * Map boolean values of [0, 1] to [true, false]
+ *
+ * @param {Object} matches field matches
+ * @param {String} field_id id of field
+ * @param {Object} field_value field object
+ *
+ * @return {Array.} array of booleans
+ */
+ 'private _getApplicable'( matches, field_id, field_value )
+ {
+ // If object is undefined, default to array of true
+ if ( typeof matches[ field_id ] === "undefined" )
+ {
+ return field_value.map( _ => true );
+ }
+
+ const indexes = matches[ field_id ].indexes;
+ const all_match = matches[ field_id ].all;
+
+ if ( indexes.length > 0 )
+ {
+ // Map indexes of 0, 1 to true, false
+ return indexes.map( x => !!x );
+ }
+ else
+ {
+ return field_value.map( _ => all_match );
+ }
+ },
+} );
diff --git a/src/field/FieldClassMatcher.js b/src/field/FieldClassMatcher.js
index 1d0153c..0c015a6 100644
--- a/src/field/FieldClassMatcher.js
+++ b/src/field/FieldClassMatcher.js
@@ -1,7 +1,7 @@
/**
- * Contains FieldClassMatcher class
+ * Reduce field predicate results into vectors and flags
*
- * Copyright (C) 2017 R-T Specialty, LLC.
+ * Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework.
*
@@ -19,12 +19,21 @@
* along with this program. If not, see .
*/
-var Class = require( 'easejs' ).Class;
+'use strict';
+
+const { Class } = require( 'easejs' );
/**
- * Generates match sets for field based on their classifications and a given
- * classification set
+ * Generate match vector for fields given field predicates and
+ * classification results
+ *
+ * TODO: Support for multiple predicates on fields is for
+ * backwards-compatibility with older classification systems; newer systems
+ * generate a single classification representing the visibility of the
+ * field, allowing the classification reduction complexity and logic to stay
+ * within TAME. Much of the complexity in this class can therefore be
+ * removed in the future.
*/
module.exports = Class( 'FieldClassMatcher',
{
@@ -40,7 +49,7 @@ module.exports = Class( 'FieldClassMatcher',
*
* @param {Object.>} fields field names and their classes
*/
- __construct: function( fields )
+ constructor( fields )
{
this._fields = fields;
},
@@ -58,97 +67,99 @@ module.exports = Class( 'FieldClassMatcher',
*
* @return {FieldClassMatcher} self
*/
- 'public match': function( classes, callback )
+ 'public match'( classes, callback )
{
- var cmatch = {};
- cmatch.__classes = classes;
-
- for ( var field in this._fields )
- {
- var cur = this._fields[ field ],
- vis = [],
- all = true,
- hasall = false;
-
- if ( cur.length === 0 )
- {
- continue;
- }
-
- // determine if we have a match based on the given classifications
- for ( var c in cur )
- {
- // if the indexes property is a scalar, then it applies to all
- // indexes
- var data = ( classes[ cur[ c ] ] || {} ),
- thisall = ( typeof data.indexes !== 'object' ),
- alltrue = ( !thisall || data.indexes === 1 );
-
- // if no indexes apply for the given classification (it may be a
- // pure boolean), then this variable will be true if any only if
- // all of them are true. Note that we only want to take the
- // value of thisall if we're on our first index, as if hasall is
- // empty thereafter, then all of them certainly aren't true!
- hasall = ( hasall || ( thisall && +c === 0 ) );
-
- // this will ensure that, if we've already determined some sort
- // of visibility, that encountering a scalar will still manage
- // to affect previous results even if it is the last
- // classification that we are checking
- var indexes = ( thisall ) ? vis : data.indexes;
-
- for ( var i in indexes )
- {
- // default to visible; note that, if we've encountered any
- // "all index" situations (scalars), then we must only be
- // true if the scalar value was true
- vis[ i ] = (
- ( !hasall || all )
- && ( ( vis[ i ] === undefined )
- ? 1
- : vis[ i ]
- )
- && this._reduceMatch(
- ( thisall ) ? data.indexes : data.indexes[ i ]
- )
- );
-
- // all are true unless one is false (duh?)
- alltrue = !!( alltrue && vis[ i ] );
- }
-
- all = ( all && alltrue );
- }
-
- // default 'any' to 'all'; this will have the effect of saying "yes
- // there are matches, but we don't care what" if there are no
- // indexes associated with the match, implying that all indexes
- // should match
- var any = all;
- for ( var i = 0, len = vis.length; i < len; i++ )
- {
- if ( vis[ i ] )
- {
- any = true;
- break;
- }
- }
-
- // store the classification match data for assertions, etc
- cmatch[ field ] = {
- all: all,
- any: any,
- indexes: vis
- };
- }
-
// currently not asynchronous, but leaves open the possibility
- callback( cmatch );
+ callback(
+ Object.keys( this._fields ).reduce(
+ ( cmatch, id ) =>
+ {
+ cmatch[ id ] = this._reduceFieldMatches(
+ this._fields[ id ],
+ classes
+ ), cmatch;
+
+ return cmatch;
+ },
+ { __classes: classes }
+ )
+ );
return this;
},
+ /**
+ * Reduce field class matches to a vector
+ *
+ * All field predicates in FIELDC will be reduced and combined into a
+ * single vector representing the visibility of each index.
+ *
+ * @param {Array} fieldc field predicate class names
+ * @param {Object} classes cmatch results
+ *
+ * @return {Object} all, any, indexes
+ */
+ 'private _reduceFieldMatches'( fieldc, classes )
+ {
+ const vis = [];
+
+ let all = true;
+ let hasall = false;
+
+ // determine if we have a match based on the given classifications
+ for ( let c in fieldc )
+ {
+ // if the indexes property is a scalar, then it applies to all
+ // indexes
+ const data = ( classes[ fieldc[ c ] ] || {} );
+ const thisall = !Array.isArray( data.indexes );
+
+ let alltrue = ( !thisall || data.indexes === 1 );
+
+ // if no indexes apply for the given classification (it may be a
+ // pure boolean), then this variable will be true if any only if
+ // all of them are true. Note that we only want to take the
+ // value of thisall if we're on our first index, as if hasall is
+ // empty thereafter, then all of them certainly aren't true!
+ hasall = ( hasall || ( thisall && +c === 0 ) );
+
+ // this will ensure that, if we've already determined some sort
+ // of visibility, that encountering a scalar will still manage
+ // to affect previous results even if it is the last
+ // classification that we are checking
+ const indexes = ( thisall ) ? vis : data.indexes;
+
+ for ( let i in indexes )
+ {
+ // default to visible; note that, if we've encountered any
+ // "all index" situations (scalars), then we must only be
+ // true if the scalar value was true
+ vis[ i ] = +(
+ ( !hasall || all )
+ && ( ( vis[ i ] === undefined )
+ ? 1
+ : vis[ i ]
+ )
+ && this._reduceMatch(
+ ( thisall ) ? data.indexes : data.indexes[ i ]
+ )
+ );
+ }
+
+ alltrue = alltrue && vis.every( x => x );
+ all = ( all && alltrue );
+ }
+
+ // store the classification match data for assertions, etc
+ return {
+ all: all,
+ any: all || vis.some( x => !!x ),
+ indexes: vis
+ };
+ },
+
+
/**
* Reduces the given scalar or vector
*
@@ -164,27 +175,13 @@ module.exports = Class( 'FieldClassMatcher',
*
* @return {number} 0 if false otherwise 1 for true
*/
- 'private _reduceMatch': function( result )
+ 'private _reduceMatch'( result )
{
- if ( ( result === undefined )
- || ( result === null )
- || ( result.length === undefined )
- )
+ if ( !Array.isArray( result ) )
{
- return result;
+ return !!result;
}
- var ret = false,
- i = result.length;
-
- // reduce with logical or
- while( i-- )
- {
- // recurse just in case we have another array of values
- ret = ret || this._reduceMatch( result[ i ] );
- }
-
- return +ret;
+ return +result.some( x => this._reduceMatch( x ) );
}
} );
-
diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js
index 17fda41..1229829 100644
--- a/src/server/daemon/controller.js
+++ b/src/server/daemon/controller.js
@@ -53,6 +53,14 @@ const {
DataApiManager,
},
+ document: {
+ DocumentProgramFormatter,
+ },
+
+ field: {
+ FieldClassMatcher,
+ },
+
server: {
DocumentServer,
@@ -450,6 +458,23 @@ function doRoute( program, request, data, resolve, reject )
} );
} );
}
+ else if ( cmd == 'progdata' )
+ {
+ acquireReadLock( quote_id, request, function()
+ {
+ handleRequest( function( quote )
+ {
+ const response = UserResponse( request );
+ const bucket = quote.getBucket();
+ const class_matcher = FieldClassMatcher( program.whens );
+
+ DocumentProgramFormatter( program, class_matcher )
+ .format( bucket )
+ .then( data => response.ok( data ) )
+ .catch( e => response.error( e ) );
+ } );
+ } );
+ }
else if ( cmd === 'mkrev' )
{
// the database operation for this is atomic and disjoint from
diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js
new file mode 100644
index 0000000..88af3d2
--- /dev/null
+++ b/test/document/DocumentProgramFormatterTest.js
@@ -0,0 +1,287 @@
+/**
+ * Test of DocumentProgramFormatter
+ *
+ * Copyright (C) 2018 R-T Specialty, LLC.
+ *
+ * This file is part of liza.
+ *
+ * liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+'use strict';
+
+const chai = require( 'chai' );
+const expect = chai.expect;
+
+const {
+ document: {
+ DocumentProgramFormatter: Sut,
+ },
+} = require( '../../' );
+
+chai.use( require( 'chai-as-promised' ) );
+
+
+describe( 'DocumentProgramFormatter', () =>
+{
+ it( "formats bucket data by steps, groups and fields", () =>
+ {
+ const bucket_data = {
+ alcohol_shown: [ "foo", "" ],
+ alcohol_not_shown: [ "" ],
+ ecigs_shown_twice: [ "foo", "bar" ],
+ ecigs_not_shown: [ "" ],
+ field_no_label: [ "" ],
+ field_no_vis: [ "true" ],
+ field_empty_array_any_true: [ "bar" ],
+ field_empty_array_any_false: [ "" ]
+ };
+
+ const expected_object = {
+ steps: [
+ {
+ title: "Manage Quote",
+ groups: []
+ },
+ {
+ title: "General Information",
+ groups: [
+ {
+ id: "group_one",
+ title: "Group One",
+ link: "locations",
+ questions: [
+ {
+ id: "alcohol_shown",
+ label: "Alcohol?",
+ value: [ "foo", "" ],
+ type: "noyes",
+ applicable: [ true, false ]
+ },
+ {
+ id: "alcohol_not_shown",
+ label: "No alcohol?",
+ value: [ "" ],
+ type: "noyes",
+ applicable: [ false ]
+ },
+ {
+ id: "field_no_vis",
+ label: "Is this field found in FieldMatcher?",
+ value: [ "true" ],
+ type: "noyes",
+ applicable: [ true ]
+ }
+ ]
+ },
+ {
+ id: "group_two",
+ title: "",
+ link: "",
+ questions: [
+ {
+ id: "ecigs_shown_twice",
+ label: "Two ecig answers?",
+ value: [ "foo", "bar" ],
+ type: "noyes",
+ applicable: [ true, true ]
+ },
+ {
+ id: "ecigs_not_shown",
+ label: "No ecigs?",
+ value: [ "" ],
+ type: "noyes",
+ applicable: [ false ]
+ },
+ {
+ id: "field_empty_array_any_true",
+ label: "Empty match array?",
+ value: [ "bar" ],
+ type: "noyes",
+ applicable: [ true ]
+ },
+ {
+ id: "field_empty_array_any_false",
+ label: "Empty match array and 'any' is false?",
+ value: [ "" ],
+ type: "noyes",
+ applicable: [ false ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ };
+
+ const bucket = createStubBucket( bucket_data );
+ const program = createStubProgram();
+ const class_matcher = createStubClassMatcher();
+
+ return expect(
+ Sut( program, class_matcher ).format( bucket )
+ ).to.eventually.deep.equal( expected_object );
+ } );
+} );
+
+
+function createStubClassMatcher()
+{
+ return {
+ match( _, callback )
+ {
+ callback({
+ "__classes": {
+ '--vis-foo': { is: true, indexes: [1,0] },
+ },
+ "alcohol_shown": {
+ "all": false,
+ "any": true,
+ "indexes": [1, 0]
+ },
+ "alcohol_not_shown": {
+ "all": false,
+ "any": false,
+ "indexes": [0]
+ },
+ "ecigs_shown_twice": {
+ "all": false,
+ "any": true,
+ "indexes": [1, 1]
+ },
+ "ecigs_not_shown": {
+ "all": false,
+ "any": false,
+ "indexes": []
+ },
+ "field_empty_array_any_false": {
+ "all": false,
+ "any": false,
+ "indexes": []
+ },
+ "field_empty_array_any_true": {
+ "all": true,
+ "any": true,
+ "indexes": []
+ },
+ }) ;
+ }
+ }
+}
+
+
+function createStubBucket( metadata )
+{
+ return {
+ getDataByName: name => metadata[ name ],
+ getData()
+ {
+ return metadata;
+ },
+ };
+}
+
+
+function createStubProgram()
+{
+ return {
+ steps: [
+ {
+ title: "Index 0"
+ },
+ {
+ title: "Manage Quote",
+ groups: []
+ },
+ {
+ title: "General Information",
+ groups: [ 'group_one', 'group_two' ]
+ }
+ ],
+ classify( bucket_data )
+ {
+ return {}
+ },
+ groups:
+ {
+ 'group_one':
+ {
+ title: "Group One",
+ link: "locations"
+ },
+ 'group_two': {},
+ },
+ fields:
+ {
+ alcohol_shown:
+ {
+ label: "Alcohol?",
+ type: "noyes",
+ required: "true",
+ },
+ alcohol_not_shown:
+ {
+ label: "No alcohol?",
+ type: "noyes",
+ required: "true"
+ },
+ ecigs_shown_twice:
+ {
+ label: "Two ecig answers?",
+ type: "noyes",
+ required: "true"
+ },
+ ecigs_not_shown:
+ {
+ label: "No ecigs?",
+ type: "noyes",
+ required: "true"
+ },
+ field_empty_array_any_true:
+ {
+ label: "Empty match array?",
+ type: "noyes",
+ required: "true"
+ },
+ field_empty_array_any_false:
+ {
+ label: "Empty match array and 'any' is false?",
+ type: "noyes",
+ required: "true"
+ },
+ field_no_vis:
+ {
+ label: "Is this field found in FieldMatcher?",
+ type: "noyes",
+ required: "true"
+ }
+ },
+ groupExclusiveFields:
+ {
+ 'group_one': [
+ "alcohol_shown",
+ "alcohol_not_shown",
+ "field_no_label",
+ "field_no_vis"
+ ],
+ 'group_two': [
+ "ecigs_shown_twice",
+ "ecigs_not_shown",
+ "field_empty_array_any_true",
+ "field_empty_array_any_false"
+ ],
+ },
+ };
+}
diff --git a/test/field/FieldClassMatcherTest.js b/test/field/FieldClassMatcherTest.js
new file mode 100644
index 0000000..fb0190f
--- /dev/null
+++ b/test/field/FieldClassMatcherTest.js
@@ -0,0 +1,485 @@
+/**
+ * Test case for FieldVisibilityEventHandler
+ *
+ * Copyright (C) 2018 R-T Specialty, LLC.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+const expect = require( 'chai' ).expect;
+
+const { FieldClassMatcher: Sut } = require( '../../' ).field;
+
+
+describe( 'FieldClassMatcher', () =>
+{
+ it( "echoes provided classes as __classes", done =>
+ {
+ const classes = { foo: [ 1 ], bar: 0 };
+
+ Sut( {} ).match( classes, result =>
+ {
+ expect( result.__classes ).to.deep.equal( classes );
+ done();
+ } );
+ } );
+
+
+ [
+ {
+ label: "does nothing with no fields or classes",
+ fields: {},
+ classes: {},
+ expected: {},
+ },
+
+ {
+ label: "sets all on >0 scalar cmatch",
+ fields: { foo: [ 'foowhen' ] },
+ classes: { foowhen: { indexes: 1 } },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [],
+ },
+ },
+ },
+
+ {
+ label: "sets none on 0 scalar cmatch",
+ fields: { foo: [ 'foowhen' ] },
+ classes: { foowhen: { indexes: 0 } },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [],
+ },
+ },
+ },
+
+ {
+ label: "sets all on all vector cmatch",
+ fields: { foo: [ 'foowhen' ] },
+ classes: { foowhen: { indexes: [ 1, 1 ] } },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1 ],
+ },
+ },
+ },
+
+ {
+ label: "sets any on partial vector cmatch",
+ fields: { foo: [ 'foowhen' ] },
+ classes: { foowhen: { indexes: [ 0, 1 ] } },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 0, 1 ],
+ },
+ },
+ },
+
+ {
+ label: "sets nothing on empty vector cmatch",
+ fields: { foo: [ 'foowhen' ] },
+ classes: { foowhen: { indexes: [ 0, 0 ] } },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [ 0, 0 ],
+ },
+ },
+ },
+
+
+ // bad class data
+ {
+ label: "handles missing class as if scalar 0",
+ fields: { foo: [ 'noexist' ] },
+ classes: {},
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [],
+ },
+ },
+ },
+
+ // multiple classes
+ {
+ label: "logical ANDs multiple vector classes (any)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 1, 0 ] },
+ foowhen2: { indexes: [ 1, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 1, 0 ],
+ },
+ },
+ },
+ {
+ label: "logical ANDs multiple vector classes (all)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 1, 1 ] },
+ foowhen2: { indexes: [ 1, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1 ],
+ },
+ },
+ },
+ {
+ label: "logical ANDs multiple vector classes (none)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 0, 0 ] },
+ foowhen2: { indexes: [ 0, 0 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [ 0, 0 ],
+ },
+ },
+ },
+
+ {
+ label: "assumes match when ANDing varying lengths (any)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 0 ] },
+ foowhen2: { indexes: [ 0, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 0, 1 ],
+ },
+ },
+ },
+ {
+ label: "assumes match when ANDing varying lengths (all)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 1 ] },
+ foowhen2: { indexes: [ 1, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1 ],
+ },
+ },
+ },
+ {
+ label: "assumes match when ANDing varying lengths (none)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 0 ] },
+ foowhen2: { indexes: [ 0, 0 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [ 0, 0 ],
+ },
+ },
+ },
+
+ {
+ label: "logically ANDs scalar with vector (any)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: 1 },
+ foowhen2: { indexes: [ 0, 1, 0 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 0, 1, 0 ],
+ },
+ },
+ },
+ {
+ label: "logically ANDs scalar with vector (all)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: 1 },
+ foowhen2: { indexes: [ 1, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1 ],
+ },
+ },
+ },
+ {
+ label: "logically ANDs scalar with vector (none)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: 0 },
+ foowhen2: { indexes: [ 1, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [ 0, 0 ],
+ },
+ },
+ },
+
+ {
+ label: "logically ANDs scalar with vector (rev, any)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 0, 1, 0 ] },
+ foowhen2: { indexes: 1 },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 0, 1, 0 ],
+ },
+ },
+ },
+ {
+ label: "logically ANDs scalar with vector (rev, all)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 1, 1 ] },
+ foowhen2: { indexes: 1 },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1 ],
+ },
+ },
+ },
+ {
+ label: "logically ANDs scalar with vector (rev, none)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ 1, 1 ] },
+ foowhen2: { indexes: 0 },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [ 0, 0 ],
+ },
+ },
+ },
+
+ {
+ label: "logically ANDs scalar with scalar (all)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: 1 },
+ foowhen2: { indexes: 1 },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [],
+ },
+ },
+ },
+ {
+ label: "logically ANDs scalar with scalar (none 0)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: 0 },
+ foowhen2: { indexes: 0 },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [],
+ },
+ },
+ },
+ {
+ label: "logically ANDs scalar with scalar (none 1)",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: 0 },
+ foowhen2: { indexes: 1 },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [],
+ },
+ },
+ },
+
+
+ // matrix cmatch
+ {
+ label: "logically ORs matrix cmatch (single)",
+ fields: { foo: [ 'foowhen' ] },
+ classes: {
+ foowhen: { indexes: [ [ 1 ] ] },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1 ],
+ },
+ },
+ },
+ {
+ label: "logically ORs matrix cmatch (any)",
+ fields: { foo: [ 'foowhen' ] },
+ classes: {
+ foowhen: { indexes: [ [ 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 0 ] ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 1, 1, 1, 0 ],
+ },
+ },
+ },
+ {
+ label: "logically ORs matrix cmatch (all)",
+ fields: { foo: [ 'foowhen' ] },
+ classes: {
+ foowhen: { indexes: [ [ 1 ], [ 0, 1 ], [ 0, 1 ] ] },
+ },
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1, 1 ],
+ },
+ },
+ },
+ {
+ label: "logically AND's logically OR'd matrix cmatches",
+ fields: { foo: [ 'foowhen1', 'foowhen2' ] },
+ classes: {
+ foowhen1: { indexes: [ [ 0 ], [ 0, 1 ], [ 1 ] ] },
+ foowhen2: { indexes: [ [ 0 ], [ 0, 1 ], [ 0 ] ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 0, 1, 0 ],
+ },
+ },
+ },
+
+ // arbitrary nesting
+ {
+ label: "logically ORs arbitrarily nested vectors",
+ fields: { foo: [ 'foowhen' ] },
+ classes: {
+ foowhen: { indexes: [ [ [ 0 ], [ 0, 1 ] ], [ [ [ 0 ] ] ] ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: true,
+ indexes: [ 1, 0 ],
+ },
+ },
+ },
+
+
+ // multiple fields
+ {
+ label: "sets multiple fields",
+ fields: { foo: [ 'foowhen' ], bar: [ 'barwhen' ] },
+ classes: {
+ foowhen: { indexes: [ 0, 0 ] },
+ barwhen: { indexes: [ 1, 1 ] },
+ },
+ expected: {
+ foo: {
+ all: false,
+ any: false,
+ indexes: [ 0, 0 ],
+ },
+ bar: {
+ all: true,
+ any: true,
+ indexes: [ 1, 1 ],
+ },
+ },
+ },
+
+ {
+ label: "sets all for fields with no predicates",
+ fields: { foo: [] },
+ classes: {},
+ expected: {
+ foo: {
+ all: true,
+ any: true,
+ indexes: [],
+ }
+ },
+ },
+ ].forEach( ( { label, fields, classes, expected } ) =>
+ {
+ it( label, done =>
+ {
+ // no use in specifying this above every time
+ expected.__classes = classes;
+
+ Sut( fields ).match( classes, result =>
+ {
+ expect( result ).to.deep.equal( expected );
+ done();
+ } );
+ } );
+ } );
+} );