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(); + } ); + } ); + } ); +} );